Connect an MCP client

The MCP endpoint, how Claude Code and Cursor are configured, every tool with its arguments and result, error shapes, rate limits, and the context pack.

Written for
integration-developer
Roles
Requires
An API token (personal token or agent account key)
Feature
mcp

SAQ runs a Model Context Protocol (MCP) server so that AI assistants can read and change tickets, log time, and read client timesheets with the same permissions as the token they hold. This page covers the transport, client setup, and every tool.

Endpoint and transport

  • Endpoint: POST https://saq.no/mcp (Streamable HTTP, JSON-RPC 2.0). GET and DELETE answer 405.
  • One JSON-RPC message per request. A JSON array (batch) is refused with 400.
  • Methods: initialize, ping, tools/list, tools/call. Other methods answer JSON-RPC error -32601. Notifications (messages without an id) are acknowledged with 202 and no body.
  • Protocol versions: 2024-11-05, 2025-03-26 (default), and 2025-06-18. initialize reports serverInfo.name: "saq" and the tools capability only; there are no resources or prompts.
  • Authentication: Authorization: Bearer saq_YOUR_TOKEN. Cookies are ignored. See Authenticate with API tokens.
  • Browser clients: if an Origin header is sent it must be https://saq.no, and the Host header must match. Native clients (Claude Code, Cursor, the MCP inspector's proxy) send no Origin and need nothing extra.

Set up Claude Code or Cursor

An Owner or Admin can copy ready-made snippets from Settings → Channels → MCP setup; the same text is returned by GET /api/w/acme/mcp in a browser session. Replace saq_YOUR_TOKEN with a token you minted.

Claude Code (.mcp.json or your user settings):

{
  "mcpServers": {
    "saq": {
      "type": "http",
      "url": "https://saq.no/mcp",
      "headers": {
        "Authorization": "Bearer saq_YOUR_TOKEN"
      }
    }
  }
}

Cursor (.cursor/mcp.json):

{
  "mcpServers": {
    "saq": {
      "url": "https://saq.no/mcp",
      "headers": {
        "Authorization": "Bearer saq_YOUR_TOKEN"
      }
    }
  }
}

Aliases

Tool arguments accept human references instead of UUIDs; every alias resolves only within the token's workspace:

Argument Accepts
ticket, source, target ACME-184 or a ticket UUID
project project key (website) or UUID
kind, state workflow key (bug, in_progress) or UUID
assignee, viewer a person's email in this workspace, a UUID, me, or none

An unknown project answers not_found; an unknown kind, state, or person answers invalid.

Tools

Tool Scope Arguments Returns
search_tickets read q, project, kind, state, stateCategory (open/closed), assignee, viewer, queue (inbox), billingClientId (UUID or none), billingPolicy, labelIds [UUID], priorityIds [UUID], due (overdue/week/none), staleDays (1–36500), blocked (boolean), unbilledOnClose (boolean), cursor, limit (≤50) { items, nextCursor } of ticket summaries, newest activity first
get_ticket read ticket The ticket page: fields, viewers, comments, events, attachments, links, GitHub links, time, permissions. comments is omitted when no audience is readable; time is omitted for client users
list_boards read none { boards: [{ id, name, key }] }: the workspace board plus each visible project
get_board read Same filters as search_tickets without cursor and limit { columns: [{ state, total, tickets, nextCursor }] }
create_ticket write (triage when project is omitted) title (required), description, project, kind, state, assignee, priorityId, billingClientId, labelIds, startDate, dueDate, estimateMinutes, parentId The new ticket summary; without project it lands in the Inbox
update_ticket write ticket plus any of title, description, kind, state, assignee, priorityId, labelIds, startDate, dueDate, billingClientId, billingPolicy, billingMode, billingDescription, estimateMinutes The updated ticket page; each field is checked against the owner's permissions
move_ticket write (triage when leaving the Inbox) ticket, project, kind, assignee, billingClientId, confirmReset The ticket page after the move
add_comment write ticket, body (≤50 000 characters), audience (shared/internal/staff), mentions [UUID, ≤20], attachmentIds [UUID, ≤20] The created comment; mentions become viewer grants
link_tickets write source, target, kind (blocks/relates/duplicates) { id } of the link
log_time write ticket, spentOn (YYYY-MM-DD), minutes (≥1), startedAt (HH:MM), note, billable (default true) { id } of the entry, logged as the token's owner
close_period none Always refused: forbidden, "Period close is a UI-only action in v1"
create_billing_run none Always refused: forbidden, "Billing runs are a UI-only action in v1"
get_bank_balances read none { banks, projectUsage }; balances need client-wide billing document access, otherwise only projectUsage
get_timesheet read documentId or clientId With documentId: the document's metadata and content (encoding: "base64" for PDF, "utf-8" for HTML and CSV). Without: the list of documents the owner may download, optionally for one client
get_inbox read review (boolean) { items }: the shared Inbox (triage scope, unrestricted token) or, with review: true, the restricted review items the owner may read
get_context_pack read none The context pack described below
link_github write ticket, repo (owner/name), url, sha (7–40 hex) and/or pr (integer) The stored GitHub link
search_docs any query (required), audience (user/admin/integration-developer), section, feature, limit (≤20) { hits }: matching documentation sections with url, title, heading, text, audience, roles, and permissions
get_doc any page (a path such as time/register-time, /docs/time/register-time/, or the full URL) One documentation page with its sections
list_docs any none The documentation table of contents

The three documentation tools read this public documentation and hold no workspace data, so any token may call them regardless of scope or project restriction.

Results and errors

A successful tools/call returns one text content block whose text is the JSON result:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"id\":\"01924c3e-6b2a-7d11-9d0e-3a1f4b5c6d7e\"}"
      }
    ]
  }
}

Domain errors (validation, permission, not found, read-only) are returned as a tool result with isError: true and the same { error, message, details } body the REST API uses:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "isError": true,
    "content": [
      {
        "type": "text",
        "text": "{\"error\":\"forbidden\",\"message\":\"This token does not include that scope\",\"details\":{\"scope\":\"write\"}}"
      }
    ]
  }
}

Invalid arguments answer error: "invalid" with details.issues. An unknown tool name answers not_found. Authentication failures and rate limits are HTTP-level (401, 403, 429) with the REST error body, not JSON-RPC results. In a read-only workspace, read tools keep working and write tools answer read_only.

Rate limits

MCP shares the REST budgets: 600 requests per minute per address, 120 per token, and the workspace's plan budget shared by every token. Each request is one JSON-RPC message, so each tool call costs one request. See API conventions.

The context pack

get_context_pack returns a read-only snapshot sized for a model's context window. Every section is permission-filtered, capped at 25 items, and carries truncated: true when more exist; page the rest with search_tickets filters.

Section Content
mine Open tickets assigned to the owner
overdue Open tickets past their due date
dueSoon Open tickets due within 7 days
inbox { count, oldestCreatedAt } for the shared Inbox, or null when the token cannot see it
blocked Open tickets with unresolved blockers, each with its blocker list (own truncation flag)
stale Open tickets with no activity for 14 days (staleDays: 14 in search_tickets)
time The current period: periodStart, periodEnd, loggedMinutes, estimateMinutes
unbilledOnClose Open deliverable tickets (billed when closed) with unbilled time
banks, bankProjectUsage Time bank balances with projected overage, and per-project usage
github GitHub links on the tickets above

Attribution

Tickets and comments created through MCP are stored with source: "mcp". With a personal token, the actor is the person and the record notes that a token acted on their behalf; with an agent key, the actor is the agent account.