API documentation
Unabyss MCP API Reference
Streamable HTTP JSON-RPC API for personal context tools at mcp.unabyss.com
Last updated 10 August 2026
The Unabyss MCP server exposes a Model Context Protocol (MCP) API over HTTPS. Clients send JSON-RPC 2.0 requests to a single endpoint and receive JSON responses. The server implements MCP protocol version 2025-06-18 with tools and prompts (no resources).
Base URL
| Item | Value |
|---|---|
| MCP endpoint | https://mcp.unabyss.com |
| Protocol | MCP Streamable HTTP (JSON-RPC 2.0) |
| MCP version | 2025-06-18 |
| Server info | unabyss-mcp 0.3.0 |
| Content-Type | application/json |
| Accept | application/json (recommended) or application/json, text/event-stream |
All requests use POST with a JSON object body. There is no REST-style path per tool; tools are invoked through the JSON-RPC tools/call method.
Configure clients with the bare origin above. Any path under the host resolves to the same JSON-RPC entrypoint, but the OAuth resource identifier is exactly https://mcp.unabyss.com — a client that derives its RFC 8707 resource from a path-suffixed endpoint URL such as https://mcp.unabyss.com/mcp is rejected with invalid_target at the authorize step.
Responses are always a single JSON document — the server never streams SSE frames, even when the client advertises text/event-stream.
Authentication
Static bearer tokens and OAuth-issued access tokens coexist on the same MCP host. Send either as a bearer on every JSON-RPC request:
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json
| Credential | Prefix | How you get it |
|---|---|---|
| Static token | unby_mcp_ |
Generate at app.unabyss.com/mcp |
| OAuth access token | unabyss_mcp_oauth_ |
Authorization-code + PKCE flow (below) |
| OAuth refresh token | unabyss_mcp_oauth_refresh_ |
Returned with the access token; use only at /oauth/token |
Lookup is prefix-routed: one indexed hash lookup for the matching token type. Both credential kinds share the same tool surface, permissions model, throttles, and credit preauth.
Static bearer tokens
- Sign in at app.unabyss.com/mcp.
- Click Generate token and copy the plaintext value (shown once).
- Tokens use the
unby_mcp_prefix.
Revoke or edit token permissions from the same page.
OAuth 2.1
Unabyss runs a self-hosted OAuth 2.1 authorization server on the MCP host (authorization code + PKCE, public clients, dynamic client registration). Aligns with the MCP authorization spec revision 2025-11-25.
Discovery metadata and protocol endpoints:
| Endpoint | Purpose |
|---|---|
/.well-known/oauth-protected-resource |
RFC 9728 protected-resource metadata |
/.well-known/oauth-authorization-server |
RFC 8414 authorization-server metadata |
/oauth/register |
Dynamic client registration (RFC 7591) |
/oauth/register/{client_id} |
Registered client metadata read (RFC 7592) |
/oauth/authorize |
Authorization request (PKCE S256 required) |
/oauth/token |
Authorization-code exchange and refresh |
/oauth/revoke |
Token revocation (RFC 7009) |
Flow (typical MCP client):
- Read
/.well-known/oauth-protected-resource(resource =https://mcp.unabyss.com) and followauthorization_serversto the authorization-server metadata. - Register via
/oauth/registerif the client has noclient_idyet (or use a Client ID Metadata Document when supported). - Open
/oauth/authorizewithresponse_type=code, PKCE (code_challenge_method=S256), and the requestedscope. - The user signs in and consents in the Unabyss app (
/mcp/consent). On approve, the authorize endpoint redirects back to the client'sredirect_uriwith an authorization code. - Exchange the code at
/oauth/tokenfor an access token (unabyss_mcp_oauth_…) and refresh token (unabyss_mcp_oauth_refresh_…). - Call the MCP JSON-RPC endpoint with
Authorization: Bearer unabyss_mcp_oauth_…. Refresh via/oauth/token(grant_type=refresh_token) before the access token expires.
| Setting | Default |
|---|---|
| Scopes | read, write |
| Grants | authorization_code, refresh_token |
| PKCE | S256 (required) |
| Access token lifetime | 4 hours (expires_in on the token response) |
| Refresh token lifetime | 30 days |
| Resource / audience | https://mcp.unabyss.com (RFC 8707), exact match — take it from /.well-known/oauth-protected-resource, not from your endpoint URL |
A 401 on the MCP host includes:
- Header:
WWW-Authenticate: Bearer realm="mcp", resource_metadata="https://mcp.unabyss.com/.well-known/oauth-protected-resource" - Body field
error.data.auth_action:"refresh"— access token expired; use the refresh token"reauthorize"— token revoked, unknown, audience mismatch, or malformed; run the OAuth flow again
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"message": "Unauthorized",
"data": {
"code": "unauthorized",
"auth_action": "refresh"
}
}
}
Refresh-token rotation supports a short grace window for concurrent refreshes (default 60 seconds). Replay within that window returns the same token response; after it, reuse is treated as a security event and the token family is revoked.
Manage connected OAuth clients alongside static tokens at app.unabyss.com/mcp.
Token permissions
Each credential carries its own redaction settings, applied to the context a tool can reach:
- Exclude private info — drops personal-life material from results.
- Exclude company confidential — drops company-confidential material from results.
- Excluded apps — per-integration blocklist; excluded sources never enter a
queryoragentic_queryanswer.
Two credentials for the same account can therefore return different answers to the same question.
JSON-RPC envelope
Every request body is a JSON object:
{
"jsonrpc": "2.0",
"id": 1,
"method": "<method>",
"params": {}
}
Successful responses:
{
"jsonrpc": "2.0",
"id": 1,
"result": {}
}
Errors come in two classes.
Protocol errors (unparseable body, missing method, unknown method) return HTTP 200 with a standard JSON-RPC numeric code and no data:
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32601, "message": "Unknown method: 'resources/list'" }
}
Codes used: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params.
Application errors (auth, throttling, validation, tool failures) use an HTTP status that matches the error class and always carry the numeric code -32603 plus a machine-readable data.code:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"message": "Human-readable message",
"data": {
"code": "unauthorized",
"auth_action": "reauthorize"
}
}
}
data may also carry auth_action (unauthorized), retry_after_seconds (rate limits, temporary unavailability), topup_url and preauth_amount (quota_exceeded), or limit_kind and upgrade_url (tier_limit_exceeded). See Error codes below.
Tool results never use the isError: true flag — a failing tool raises an application error envelope instead.
Methods
| Method | Purpose |
|---|---|
initialize |
Handshake; returns server info, capabilities, and instructions |
tools/list |
Discover callable tools and their input schemas |
tools/call |
Invoke a tool |
prompts/list |
List prompt descriptors (setup guide + one per active skill) |
prompts/get |
Fetch a prompt's messages by name |
Any other method returns -32601.
initialize
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {
"name": "my-client",
"version": "1.0.0"
}
}
}
Result (abbreviated)
{
"protocolVersion": "2025-06-18",
"serverInfo": {
"name": "unabyss-mcp",
"version": "0.3.0"
},
"capabilities": {
"tools": { "listChanged": false },
"prompts": { "listChanged": false }
},
"instructions": "..."
}
clientInfo.name is recorded against the credential and used to scope the setup state described in First-connection setup below. The instructions string varies with that state.
tools/list
Returns the tools the authenticated credential may call. Use this to verify credentials without running a heavy tool.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
Result (shape)
{
"tools": [
{
"name": "query",
"description": "...",
"inputSchema": { "type": "object", "properties": {}, "required": [] },
"annotations": { "readOnlyHint": true, "destructiveHint": false, "openWorldHint": false }
}
]
}
The three skill tools (list_skills, get_skill, read_skill_file) are omitted from the catalog when no active skills exist.
tools/call
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "whoami",
"arguments": {}
}
}
Result (shape)
{
"content": [
{
"type": "text",
"text": "Answer or JSON string"
}
],
"isError": false,
"structuredContent": {}
}
content[0].text— primary text payload (always present for successful calls).structuredContent— typed JSON, returned only bylist_integrations,export_list, andlist_skills. Those tools repeat the same JSON incontent[0].text, because many hosts only forward text to the model.- Other tools that return structured data (
store,agentic_query,export_read,propose_connection) serialize it as a JSON string incontent[0].text. - Text payloads occasionally carry an appended notice — setup guidance, or a trial-expiry note on plan-limited accounts.
prompts/list and prompts/get
prompts/list returns the reserved unabyss-setup descriptor plus one descriptor per active skill (the prompt name is the skill slug). prompts/get takes {"name": "<slug>"} and returns {"messages": [...]}. Fetching a skill prompt counts as a skill use, exactly like get_skill.
First-connection setup
Unabyss runs a one-time, per-credential setup flow via unabyss_setup. The counted steps are numbered Step N of 3 (apps → preferences → system prompt). Skills are not part of the flow.
| Step | Purpose |
|---|---|
step1 (default) |
Intro / offer setup |
step2 |
Connect apps (list_integrations / propose_connection); requires ≥2 connected integrations before later steps continue |
step3 |
What to save back to Unabyss |
step4 |
Client system prompt; pass context_preference from step3 (save_everything | save_important | save_nothing) |
first-prompt |
Normal finish: pass instructions_installed (bool); records setup done, unblocks tools, offers a first action |
completed / skipped |
Early exit only when the user explicitly chooses |
If the account already has ≥2 connected apps, step2 is skipped and the intro previews the short path. Steps past apps redirect back to step2 until that bar is met. Off-flow skill steps (step5, all-skills, essential-skills, <category>-skills) stay callable on request but are not linked from the main path.
Tool gate while setup is unresolved
While a connection's setup is unresolved and at least one active skill exists, tools/call behaves differently:
- These tools always run:
unabyss_setup,list_integrations,propose_connection,list_skills,get_skill,read_skill_file. - Every other tool returns setup guidance as a normal text result (HTTP 200) — not an error — so clients don't mistake it for an auth failure.
list_integrationsstill answers during setup, but with reduced{name, slug}rows plus agent-facing notes.
The gate resolves when setup is recorded as done (first-prompt or completed) or skipped. Escape hatch: once setup has been started and the skip offer has been shown, the next gated call records skipped automatically and runs normally from then on. The gate never re-arms for that credential.
Tools
Call tools/list for the live catalog including input schemas.
| Tool | Purpose | Arguments |
|---|---|---|
whoami |
Return the user's identity summary | none |
query |
Natural-language lookup against stored context | question (string, max 8 KB) |
agentic_query |
Deep multi-step synthesis; may return pending + query_id |
question (string, max 8 KB) |
agentic_query_read |
Poll a pending agentic_query result |
query_id (UUID string) |
store |
Persist durable text to the knowledge base | memory (string, max 32 KB) |
update_identity |
Replace the identity summary | content (string) |
list_integrations |
List connected integrations and connectable catalog apps | none |
propose_connection |
Mint a one-click connect URL for an app | app (slug), agent_name ([a-zA-Z0-9_-], max 64) |
export_list |
List markdown exports | search (optional), limit (optional int) |
export_read |
Read full markdown for a ready export | export_id (UUID string) |
export_create |
Generate a new export in the background | topic or preset_slug (not both) |
export_create_from_text |
Save markdown as an export instantly | content (required, max 200 KB), title, topic_text |
export_refresh |
Regenerate an existing export | export_id (UUID string) |
list_skills |
List available skills (playbooks) | none |
get_skill |
Load a skill's markdown or .skill bundle link |
slug, type (markdown | skill-file) |
read_skill_file |
Read a skill's supporting file | slug, path |
unabyss_setup |
Drive the optional one-time setup flow | step (optional), context_preference (optional, for step4), instructions_installed (optional bool, for first-prompt) |
Example: whoami
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "whoami",
"arguments": {}
}
}
Example: query
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "query",
"arguments": {
"question": "What projects am I working on?"
}
}
}
The answer is plain text in content[0].text.
Example: store
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "store",
"arguments": {
"memory": "Prefers OAuth over static API keys for third-party integrations."
}
}
}
Writes are asynchronous; the call acknowledges the enqueue as a JSON string:
{
"content": [{ "type": "text", "text": "{\"status\": \"queued\"}" }],
"isError": false
}
Example: list_integrations
No arguments. Returns two arrays: connected (one row per live connection with sync metadata) and available (catalog apps the user has not connected yet).
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_integrations",
"arguments": {}
}
}
Structured response:
{
"content": [
{
"type": "text",
"text": "{\"connected\":[...],\"available\":[...]}"
}
],
"isError": false,
"structuredContent": {
"connected": [
{
"slug": "github",
"display_name": "GitHub",
"icon_url": null,
"kinds": ["github_repo"],
"items_count": 12,
"last_sync_date": "2026-06-01T10:00:00Z",
"kind": "oauth",
"uid": "42",
"id": null,
"pipedream_account_id": null
}
],
"available": [
{
"slug": "gmail",
"display_name": "Gmail",
"icon_url": null,
"connect_mechanism": "pipedream",
"available": true,
"unavailable_message": null
}
]
}
}
Use available[].slug as the app argument to propose_connection. There is no include_all parameter — both lists are always returned.
Example: propose_connection
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "propose_connection",
"arguments": { "app": "gmail", "agent_name": "my-client" }
}
}
Returns a discriminated payload as a JSON string. Business outcomes are successful results, not errors:
{
"status": "ok",
"reason": null,
"connection": {
"app": "gmail",
"display_name": "Gmail",
"icon_url": null,
"connect_url": "https://api.unabyss.com/api/integrations/mcp-connect/<token>/",
"expires_at": "2026-07-27T12:00:00Z",
"intro": ""
}
}
status is one of ok, already_connected, unknown_app, not_connectable, or temporarily_unavailable. On any non-ok status, connection is null and reason explains why.
connect_url is always an Unabyss link — never a provider or vendor URL. For one-click apps it is a redirect endpoint that resolves the real destination (the provider's OAuth screen, or a Pipedream connect link minted at that moment) when the user opens it; apps that must be set up in the UI get a /connections deep link instead. Give it to the user promptly: when expires_at is set it is the expiry of that link itself, so a link handed over much later may already be dead.
Exports
export_list returns {"exports": [{id, title, topic_text, status, kb_stale}]} in structuredContent (default page size 5; raise it with limit, narrow it with search).
export_read returns {id, title, topic_text, status, markdown} as a JSON string, and fails with resource_not_ready unless the export status is ready.
export_create and export_refresh enqueue multi-minute background generation and return a short confirmation with the export ID; call them only on explicit user request. Refreshing an export that is already generating returns conflict. export_create_from_text is instant and free — it stores the markdown you pass in.
Skills
list_skills returns {"skills": [{slug, name, description}]} in structuredContent. get_skill returns the playbook markdown plus its supporting-file manifest, or — with type: "skill-file" — a link to the packaged .skill bundle for clients that install skills by file upload. read_skill_file fetches one supporting file by slug + path. Unknown slugs and paths return resource_not_found.
Agentic query polling
agentic_query waits a few seconds for an inline answer. If the job is still running it returns a pending handle:
{
"status": "pending",
"query_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"poll_after_seconds": 5
}
Poll agentic_query_read with the same query_id until status is completed or failed. Both tools serialize this payload as a JSON string in content[0].text.
Error codes
Machine-readable codes appear in error.data.code:
| Code | HTTP | Meaning |
|---|---|---|
invalid_request |
400 | Malformed JSON-RPC body or unknown tool |
unauthorized |
401 | Missing, invalid, revoked, or expired bearer; see data.auth_action (refresh or reauthorize) |
query_input_invalid |
400 | Tool argument validation failed |
store_payload_rejected |
400 | Store payload empty, too large, or invalid |
quota_exceeded |
402 | Insufficient Unabyss credits; data.topup_url points at billing |
setup_required |
403 | Account storage mode does not allow MCP (cloud storage required); first-connection setup uses a soft text gate, not this code |
resource_not_found |
404 | Export, query, skill, or skill file not found |
resource_not_ready |
409 | Export not ready for read |
conflict |
409 | Operation already in flight (e.g. export regenerating) |
rate_limit_exceeded |
429 | Rate limit hit; retry after Retry-After |
memory_persist_failed |
500 | Write accepted but not persisted |
internal_error |
500 | Unexpected server error |
tier_limit_exceeded |
500 | Plan limit reached; data.limit_kind and data.upgrade_url explain which |
memory_unavailable |
503 | Memory backend temporarily unavailable |
service_temporarily_unavailable |
503 | Dependency degraded; retry after the hint |
Rate-limited and temporarily-unavailable responses also set the Retry-After header. If the server is disabled for maintenance, requests return a bodyless 503 with Retry-After: 60.
Rate limits and credits
Default per-credential caps (per token unless noted):
| Scope | Limit |
|---|---|
query, whoami, and other read tools |
1000 / hour |
agentic_query |
60 / hour |
store |
25 / hour, and 50 / day per account |
export_create, export_create_from_text, export_refresh |
10 / hour |
query, store, and agentic_query are billed against Unabyss credits, with agentic_query costing several times a plain query. whoami, update_identity, the integration tools, the export tools, and the skill tools are free. Plans also cap total MCP calls and deep queries — those rejections come back as tier_limit_exceeded rather than rate_limit_exceeded.
Support
- Product and token management: app.unabyss.com/mcp
- General inquiries: unabyss.com
- MCP protocol specification: modelcontextprotocol.io