MCP server
A remote Model Context Protocol server, so any MCP-capable agent can read and write Sunny projects, rooms, catalog, and quotes over the network.
Endpoint
Section titled “Endpoint”POST https://agents.sunnysdk.com/mcp — stateless
Streamable HTTP:
one JSON-RPC request per call, no session id. Responses come back as plain JSON, not an SSE
stream. GET and DELETE return 405.
Content-Type: application/jsonAccept: application/json, text/event-streamAuthorization: Bearer <access token>No tool spends model tokens. There is no arrange, plan-floor, or AI preview render over MCP. Every tool is a read, a deterministic render, or a write that changes stored data.
Connecting
Section titled “Connecting”-
Find the authorization server.
GET /.well-known/oauth-protected-resourceonagents.sunnysdk.comreturns RFC 9728 metadata naming the authorization server and its scopes. A 401 from/mcpcarries the same URL in aWWW-Authenticateheader, so a compliant client can find it with no out-of-band config.GET https://agents.sunnysdk.com/.well-known/oauth-protected-resource {"resource": "https://agents.sunnysdk.com/mcp","authorization_servers": ["https://auth.sunnysdk.com/api/auth"],"bearer_methods_supported": ["header"],"scopes_supported": ["openid","profile","email","offline_access","projects:read","projects:write","quotes:read","quotes:write","quote_costs:read","orders:write","arrange","workflows:run","workflows:read","catalog:read","catalog:write","docs:read","embeds:write","embeds:read","webhooks:manage","org:admin","audit:read","admin"]}A server card for pre-connection discovery is also served at
/.well-known/mcp/server-card.json. -
Register a client. Preferred: host a JSON Client ID Metadata Document at a public HTTPS URL (MCP 2026-07-28 profile — the document needs
client_nameandredirect_uris) and use that URL as yourclient_iddirectly; no registration call needed. See Registering a client for the document shape. RFC 7591 Dynamic Client Registration atPOST https://auth.sunnysdk.com/api/auth/oauth2/registerstill works as a fallback for a client that can’t serve a metadata document. -
Authorize. OAuth 2.1 authorization code with PKCE, with
resource=https://agents.sunnysdk.com/mcp. The user consents on Sunny every time; a grant is never silently re-issued. Ask for only the scopes you need — see OAuth & scopes for what each one grants. -
Call the endpoint. Every request carries the Bearer token. The MCP server accepts OAuth Bearer tokens only — no API keys, no cookies. A missing, invalid, or expired token always gets a 401.
Scope is checked on every tool call, not once at connection time. Everything is scoped to the user’s active workspace: an id belonging to another workspace reads as “Not found”, so another workspace’s data and a typo are indistinguishable by design.
Clients that support MCP Apps render some results as interactive cards (catalog search, a product card, a project, a plan, a live room). Every tool also returns a text result for clients that don’t.
Projects and rooms
Section titled “Projects and rooms”| Tool | Scope | What it does |
|---|---|---|
list_projects |
projects:read |
Projects visible to the user’s active workspace. |
get_project |
projects:read |
One project by id, with its floors and spaces. |
get_floor |
projects:read |
One floor by id. |
get_space |
projects:read |
One space (room) by id, plus its arrangement history, newest first. With projects:write, also the room as an open studio tab has it right now (liveRoom). |
export_layout |
projects:read |
A space’s current arrangement as a sunny-layout document. |
import_layout |
projects:write |
Import a sunny-layout document into a space as a new saved version. Unknown catalog ids are dropped and reported per item. |
render_plan |
projects:read |
A deterministic top-down plan as a PNG plus a text legend: one space, every room on a floor, or a sunny-layout document rendered without saving it. |
render_view |
projects:read |
A 3D view of a space as an image (iso or top). A first render of a layout can take 15–40 seconds; unchanged layouts return quickly. |
show_room |
projects:read |
A live card for one room, in 2D or 3D, that follows edits as they happen. Call it once before editing. Where the app allows, expand it to full screen to keep it in view. With only projects:read it shows the room as last saved. |
place_item |
projects:write |
Live edit: add one catalog piece at a position in meters. |
move_item |
projects:write |
Live edit: move or turn a piece by id. Only the fields you pass change. |
remove_item |
projects:write |
Live edit: remove a piece. The person can undo it from the studio. |
Live edits (place_item, move_item, remove_item) change a room while it is open in the
Sunny studio with live editing turned on (Connect AI in the studio). If no studio tab has the
room connected, the call returns isError: true saying so, and nothing changes. Take the room id
from get_project or get_floor, and piece ids from get_space’s liveRoom. When the room is
open this way, get_space, render_plan, and render_view also see its unsaved edits.
Catalog
Section titled “Catalog”| Tool | Scope | What it does |
|---|---|---|
search_catalog |
catalog:read |
Free-text and category search over the catalog. Returns text for the agent and shows the person nothing. |
inspect_catalog_item |
catalog:read |
Full detail for one catalog id: footprint, finishes, sizes, and configuration options. |
show_catalog_item |
catalog:read |
One catalog item as an interactive product card (3D view, finishes, sizes, configured price). |
show_catalog_items |
catalog:read |
Up to 12 chosen catalog items as a grid of product cards, shown once with the agent’s final picks. |
get_reference_photos |
catalog:read |
Real product photos for one or more catalog ids, so a vision-capable agent can compare them against a render or a user’s photo. |
Dealer-only items appear only to a workspace with an active owner or dealer relationship with that item’s manufacturer; to anyone else they read as unknown ids.
Product documentation
Section titled “Product documentation”| Tool | Scope | What it does |
|---|---|---|
search_docs |
docs:read |
Search product documentation — family composition rules, dimension tables, named assemblies, an item’s attach points and configuration — by free text. |
get_docs |
docs:read |
Fetch one product doc by id (family:<familyId> or item:<catalogId>): its summary and section list, or one section’s full text. |
These are about furniture products already found with search_catalog, not about this
developer documentation. docs:read is elevated: it is granted per client, never by default.
Quotes and customers
Section titled “Quotes and customers”| Tool | Scope | What it does |
|---|---|---|
search_customers |
quotes:read |
Find customers in the workspace by name or email. |
get_quote |
quotes:read |
One quote with its customer, lines, totals, and Present link. Dealer cost and margin appear only with quote_costs:read. |
preflight_order |
quotes:read |
Check an order (SIF or OFDA XML text, or the order a quote exports) against the manufacturer’s price book: part numbers, options, required choices, and prices. Reports only; never edits. |
list_discount_tables |
quote_costs:read |
The workspace’s discount tables and their revisions, so a quote can be priced against a named one. |
save_customer |
quotes:read and quotes:write |
Create or update a customer. Creating with an email that already exists returns the existing customer instead of a duplicate. |
save_quote |
quotes:read and quotes:write |
Create or update a quote, optionally with a batch of line changes. Line changes run in order and are not transactional: a failure reports what was applied. |
present_quote |
quotes:read and quotes:write |
Publish the quote’s customer-facing Present link and return its URL. Idempotent. It does not send email. |
quote_from_spaces |
quotes:write |
Create a quote seeded from several rooms of one project, one group of lines per room. |
add_room_to_quote |
quotes:write |
Append one room’s lines to an existing quote, or re-seed a room already on it. |
quotes:write and quote_costs:read are elevated scopes. Money in a quote result is in integer
minor units (cents for USD), the same as the REST /v1 API.
Embeds
Section titled “Embeds”| Tool | Scope | What it does |
|---|---|---|
manage_item_embeds |
embeds:write |
Mint, list, update, or revoke single-item embed tokens. |
item_embed_stats |
embeds:read or embeds:write |
30-day daily view, selection, and open counters for one embed token. Unknown tokens read as all-zero, never an error. |
manufacturer_embed_stats |
embeds:read or embeds:write |
30-day roll-up of embed activity across every workspace for one manufacturer. Requires the manufacturer’s owner relationship; a dealer gets an error. |
Embed tools are limited to items from manufacturers your workspace works with, as the manufacturer or as a dealer. A dealer sees only its own workspace’s tokens.
Example call
Section titled “Example call”{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "export_layout", "arguments": { "spaceId": "spc_k3m9x1q4z8f2" } }}{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "{\"format\":\"sunny-layout\",\"version\":1,...}" } ] }}A tool-level failure — wrong scope, unknown id, an invalid document — comes back as a JSON-RPC
success envelope whose result carries isError: true and a readable message. That’s the MCP
convention for business errors, and it’s distinct from a protocol error or the connection-level
401 for a bad token.
{ "content": [ { "type": "text", "text": "insufficient_scope: this tool requires the \"projects:write\" scope, which the presented token does not grant." } ], "isError": true}Rate limits
Section titled “Rate limits”These tools share a budget of 5 calls per 60 seconds per OAuth client and user:
import_layout, render_plan, render_view, manage_item_embeds, save_customer,
save_quote, present_quote, quote_from_spaces, and add_room_to_quote. It is the same budget
as the REST API’s cost endpoints, so calls on either surface count against it.
Live edits (place_item, move_item, remove_item) have their own limit of 120 calls per 60
seconds per OAuth client, user, and room.
Tools that read or write through the REST API on your behalf (catalog search, the quote tools,
live edits, render_view) also count toward the REST API’s per-user limits.
Over a limit, the tool returns isError: true with a rate_limited: message naming the retry
window, rather than an HTTP 429.