SaaS Kit API, as shipped in the download
Ownware SaaS Kit — Control-Plane API & MCP (3.1)
The kit is a control plane, not a business app. It provisions tenants, wraps a product for each one, and keeps the audit trail. So this document is shorter than a product's, and the shape is different in one way worth saying at the top:
There is no REST API. The kit has exactly one machine door — POST /mcp — and it is read-only. Everything that changes a tenant is a human click in the console, behind a session and a CSRF token. That is a decision, not a gap, and the reasoning is under Why there is no REST API below.
Authentication
One key type: a console API key, minted in Console → Admin → API & AI (/admin/api-settings).
Authorization: Bearer ck_<id>_<secret>
The token is shown once at mint time and stored only as a SHA-256 hash, so the console cannot show it to you again — revoke and mint a new one instead. Keys are revocable individually (/admin/api-settings/revoke).
The key is an owner-level credential for your whole fleet. It is not a tenant key and there is no per-tenant scoping: a key that can list tenants can list all of them. Treat it as you would the console password.
The one machine door
| Address | POST https://<your-console-host>/mcp |
| Auth | Authorization: Bearer ck_… — 401 without it |
| Transport | MCP over streamable HTTP, stateless: one JSON-RPC 2.0 request in, one response out |
| Wrong method | GET or HEAD answers 405 with allow: POST and a JSON hint |
It lives on the console host only. The route is mounted after tenant dispatch has returned, so /mcp on a tenant subdomain can never reach the control plane — a tenant's own product answers there with its own door and its own key, and the two cannot be confused.
The 405 matters more than it looks. /your-ai-can-vet-us tells a visiting agent to run curl -sI <host>/mcp and expect 405 with allow: POST; a 404 reads as *"this host has no MCP door"*, which is what happened until 2026-09-07, when the route was matched on POST alone. Answering the wrong method without a key leaks nothing and is the difference between an agent finding the door and concluding there is none.
Why there is no REST API
ControlApi mints, authenticates and revokes keys. It does not route anything. The console's 49 registered routes (34 GET, 21 POST, across 49 distinct paths) are session-guarded HTML for a person, and the operations that matter — create a tenant, suspend one, change a plan, edit settings — are deliberately not reachable by any token.
A control plane's write operations are the ones you least want an agent to reach by accident: a mistaken tenant_delete is somebody's live business. So the kit gives an agent everything it needs to answer questions about the fleet, and nothing that can change it.
MCP — the agent door
The kit speaks the Model Context Protocol, so Claude, ChatGPT agents, n8n's AI nodes or your own code can inspect the fleet without a screen.
curl -s -X POST https://console.example.com/mcp \
-H "Authorization: Bearer ck_1_xxxx" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1"}}}'
Then tools/list to discover, tools/call to act:
curl -s -X POST https://console.example.com/mcp \
-H "Authorization: Bearer ck_1_xxxx" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"tenant_counts","arguments":{}}}'
Protocol versions accepted: 2025-06-18, 2025-03-26, 2024-11-05.
The server introduces itself with one paragraph of instructions, which says the same thing this document does: *"Read-only control-plane access: inspect the tenant fleet, registered products and the audit trail. Provisioning, suspension and settings are console-only human actions."*
Connect it to an assistant
Mint the key first: Console → Admin → API & AI.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://console.example.com/mcp |
| The key | header Authorization: Bearer ck_1_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http saas-kit https://console.example.com/mcp \
--header "Authorization: Bearer ck_1_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "saas-kit",
"server_url": "https://console.example.com/mcp",
"authorization": "ck_1_xxxx",
"require_approval": "never"
}
Own Your AI reads a list of servers in this shape:
{
"mcpServers": [
{ "id": "saas-kit", "name": "Ownware SaaS Kit", "url": "https://console.example.com/mcp",
"token": "ck_1_xxxx", "enabled": true }
]
}
**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://console.example.com/mcp \
-H "Authorization: Bearer ck_1_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: the kit implements the protocol, not an integration with a particular vendor.
Tools
Four, and every one is read-only. They are annotated readOnlyHint: true in tools/list, so a client can see it before it calls anything.
| Tool | Writes? | Arguments | What it does |
|---|---|---|---|
tenants_list | no | status (optional) | Every tenant with product, plan and status, plus the count. Filters to one status when given |
tenant_get | no | subdomain (required) | Public detail for one tenant. Returns {"error": "no such tenant", …} rather than guessing |
tenant_counts | no | none | Tenant counts grouped by status |
audit_search | no | actor, entity, action (all optional) | The control-plane audit trail, most recent first, max 100 rows. Empty filters are dropped rather than matched as blanks |
The kit's tool set is read-only by construction, not by key scope. In a product, a read-only key hides the write tools; here there are no write tools to hide, so a full-access console key and a read-only one see exactly the same four. Nothing an agent can reach changes a tenant.
Every /mcp request is written to the audit trail as mcp.request, against the key's own id and label. An agent's activity is as visible as a person's.
What an agent cannot do, and what it cannot see
Cannot do, because no tool exists: create a tenant · suspend, resume or cancel one · change a plan · register or remove a product · edit settings · mint or revoke keys · delete anything. All of those are console clicks behind a session and a CSRF token.
Cannot see: a tenant is projected to exactly five fields — subdomain, product, plan, status, created_at. Database credentials, connection strings, support-SSO material, billing identifiers and the tenant's own application data are never in an MCP response, and the projection is applied in one place so a new tool cannot widen it by accident.
A tenant's own product data is not reachable from here at all. The control plane knows that a tenant exists and which product it runs; it does not read inside it. To ask a product a question, use that product's MCP door on the tenant's own host, with a key minted there.
Operational endpoints
| Endpoint | Guard | What it does |
|---|---|---|
GET /healthz | none | Liveness: {"ok":true,"app":"saas-kit","version":"3.1.13"} |
GET /.well-known/agent.json | none | The vet facts as JSON, with the live and coming-soon counts |
GET /.well-known/security.txt | none | RFC 9116 machine-readable security contact |
GET /sitemap.xml | none | Every indexable URL on the plane |
GET /admin/tenants.csv | owner session | The tenant table as CSV |
GET /admin/audit.csv | owner session | The audit trail as CSV |
GET /admin/backup.json | owner session | Redacted control-plane backup |
GET /account/export/{sub} | tenant account session | A tenant's own export of what the plane holds about them |
/healthz, agent.json, security.txt and sitemap.xml are the only unauthenticated machine routes, and none of them names a tenant.
Limits, honestly
audit_searchreturns at most 100 rows and there is no pagination or cursor. It is for answering "what happened to this tenant recently", not for exporting the trail — use/admin/audit.csvfor that.tenants_listis unpaged. At a few hundred tenants that is fine; at a few thousand the response gets large, and the projection is the only thing keeping it small.- The version string above is the application's, reported by
/healthzand in the MCP handshake. It is not the release number on the download — those are deliberately separate across the whole catalogue, and the zip filename is the one that tracks releases. - There is no rate limit on
/mcp. The key is the control; if you need throttling, put it in front of the console.