MCP

The Model Context Protocol server at /mcp, its tools, and how clients connect to it.

The workspace runs a Model Context Protocol server at https://your-workspace.albaticket.com/mcp. An AI application connected to it discovers the tools below and calls them for its user, with that member's permissions. The tools are the REST API under other names, with the same arguments, answers, rules and errors.

The MCP server is switched on by an administrator of the workspace, separately from the REST API, and is off in a new workspace (see Overview). While it is off every request to /mcp is answered 403 feature_disabled and /.well-known/oauth-protected-resource/mcp answers 404, so a client finds nothing to connect to.

Connecting a client

A client that supports remote MCP servers over HTTP with OAuth needs only the address. It is told to sign in, registers itself and sends the member to the consent page, all by itself (see OAuth 2.1).

Client How
Claude (claude.ai, Claude Desktop) Add a custom connector with the address
Claude Code claude mcp add --transport http alba https://your-workspace.albaticket.com/mcp, then /mcp to sign in
Cursor In .cursor/mcp.json: {"mcpServers": {"alba": {"url": "https://your-workspace.albaticket.com/mcp"}}}
VS Code In .vscode/mcp.json: {"servers": {"alba": {"type": "http", "url": "https://your-workspace.albaticket.com/mcp"}}}
ChatGPT Where custom connectors are available, create one with the address and OAuth

A client that cannot run OAuth sends a personal token as a header instead:

claude mcp add --transport http alba https://your-workspace.albaticket.com/mcp \
  --header "Authorization: Bearer alba_pat_…"
{
  "mcpServers": {
    "alba": {
      "url": "https://your-workspace.albaticket.com/mcp",
      "headers": {"Authorization": "Bearer alba_pat_…"}
    }
  }
}

A client that only speaks MCP over standard input and output can reach it through a bridge such as mcp-remote.

The transport

The server speaks MCP's Streamable HTTP transport and keeps no state between requests:

  • Every message is a POST to /mcp with a JSON-RPC 2.0 body and the bearer token. Send Accept: application/json, text/event-stream.
  • A request is answered with one JSON body (Content-Type: application/json), never an event stream.
  • A notification or a response is answered 202 Accepted with no body.
  • A JSON array of messages (a batch, from the 2025-03-26 protocol) is answered with an array of the replies.
  • There is no session: no Mcp-Session-Id is issued, and any server of a clustered workspace answers any request.
  • GET and DELETE are answered 405: the server sends nothing unasked, so there is no stream to open or session to end.

Protocol versions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05 are understood. The server answers initialize with the version asked for when it knows it, and otherwise with the newest.

The server offers tools only: no resources, prompts, sampling or subscriptions. Its tool list changes only when an administrator switches a module or feature on or off for the workspace.

Tools

A token with read access is offered only the tools marked read. The tools of a module or feature that is switched off for the workspace (boards, sprints, the helpdesk, the knowledge base, time, the CRM) are not listed, the initialize instructions leave it out, and calling one all the same answers an error result that says it is switched off. Each tool carries MCP's annotations (readOnlyHint, destructiveHint, idempotentHint, and openWorldHint: false), so a client can ask its user before a change.

Tool Arguments Same as
whoami read GET /me
list_projects read GET /projects
describe_project read project GET /projects/{key}
list_request_types read project, include_archived GET /projects/{key}/request-types
get_request_type read project, request_type GET /projects/{key}/request-types/{request_type}
create_request_type project, key, name, ticket_type, description, help_text, portal_group, hidden, submission, fields POST /projects/{key}/request-types
update_request_type project, request_type, and any of create_request_type's PATCH /projects/{key}/request-types/{request_type}
reorder_request_types project, order POST /projects/{key}/request-types/order
retire_request_type project, request_type DELETE /projects/{key}/request-types/{request_type}
list_spaces read GET /knowledge/spaces
get_page_tree read space GET /knowledge/spaces/{space}/tree
get_page read space, page GET /knowledge/spaces/{space}/pages/{page}
search_pages read query, space, limit GET /knowledge/search
create_page space, title, body, kind, parent, draft, published, message POST /knowledge/spaces/{space}/pages
update_page space, page, version, and any of title, body, draft, published, message PATCH /knowledge/spaces/{space}/pages/{page}
move_page space, page, parent, before, after POST …/pages/{page}/move
archive_page space, page DELETE /knowledge/spaces/{space}/pages/{page}
search_tickets read query, project, status, assignee, reporter, label, type, sort, limit, offset GET /tickets
get_ticket read key GET /tickets/{key}
create_ticket project, summary, description, type, request_type, priority, assignee, labels, components, parent, due_date, fields, company POST /tickets
update_ticket key, and any of summary, description, type, priority, assignee, labels, components, parent, due_date, fields, company PATCH /tickets/{key}
add_comment key, body, internal POST /tickets/{key}/comments
log_work key, time_spent or time_spent_seconds, started_at, body POST /tickets/{key}/worklogs
transition_ticket key, transition, resolution, comment, comment_internal, assignee, fields POST /tickets/{key}/transitions
assign_ticket key, assignee, comment PUT /tickets/{key}/assignee
list_boards read GET /boards
get_board read board, sprint GET /boards/{number}
get_backlog read board GET /boards/{number}/backlog
list_sprints read board GET /boards/{number}/sprints
move_card board, key, column, before, after, resolution, comment, comment_internal, assignee, fields POST /boards/{number}/moves
move_to_sprint board, key, sprint, before, after POST /boards/{number}/backlog/moves
create_sprint board, name, goal, start_date, end_date POST /boards/{number}/sprints
update_sprint board, sprint, name, goal, start_date, end_date PATCH /boards/{number}/sprints/{sprint}
start_sprint board, sprint, start_date, end_date POST …/sprints/{sprint}/start
complete_sprint board, sprint, move_open_to POST …/sprints/{sprint}/complete
delete_sprint board, sprint DELETE …/sprints/{sprint}
list_activities read from, to, timezone, project, customer, ticket, tag, member, running, limit, offset GET /activities
get_activity read id GET /activities/{id}
create_activity description, start, end, duration, timezone, tags, project, customer, ticket, note, billable POST /activities
update_activity id, and any of create_activity's PATCH /activities/{id}
delete_activity id DELETE /activities/{id}
stop_activity id, end, timezone POST /activities/{id}/stop
list_organizations read GET /organizations
list_phases read customer GET /phases
report_hours read from, to, timezone, phase, customer, project, ticket, member, group_by GET /reports/hours
search_contacts read q, company, stage, owner, archived, limit, offset GET /contacts
get_contact read id GET /contacts/{id}
create_contact name, job_title, company, stage, owner, notes, email, phone POST /contacts
update_contact id, and any of name, job_title, company, stage, owner, notes PATCH /contacts/{id}
add_contact_email id, address, label, primary POST /contacts/{id}/emails
add_contact_phone id, number, label, primary POST /contacts/{id}/phones
search_companies read q, stage, owner, customers, archived, limit, offset GET /companies
get_company read id GET /companies/{id}
update_company id, and any of name, website, industry, size, stage, owner, description, domains PATCH /companies/{id}
list_interactions read contact, company or ticket, kind, limit, offset GET /interactions
log_interaction kind, subject, body, contact, company, ticket, occurred_at, duration, due_at, assignee, timezone POST /interactions
complete_follow_up id POST /interactions/{id}/complete
list_follow_ups read GET /follow-ups

The full JSON Schema of each tool's arguments is in its inputSchema, from tools/list. A tool's arguments are those of the REST operation beside it, its path, query and body parameters together, and are built from the same declaration, so the two never differ; they mean what they do in the REST API; see Tickets and projects, Boards and sprints, Helpdesk forms, Knowledge base, Time tracking and The CRM. The time tools take the member's timezone with a day or a local time, as that page explains; the tools that remove or retire something (retire_request_type, archive_page, delete_sprint and delete_activity, those whose operation is a DELETE) are marked destructive, and no other is.

The server's initialize answer carries short instructions for the model: what keys look like, which tools to call first, and that text is Markdown. Clients that pass them on need no prompt of their own to use it well.

The CRM's tools are named in the instructions only to a member who may see the CRM, and its writing tools only to a connection with write access. They say that a deal is a ticket in a sales project and that contacts' details are personal data, to be used for the task in hand.

When the workspace has a GitHub or GitLab integration switched on, the instructions also tell a coding agent to put a ticket's key in its branch, commits and pull request, so the work is linked to the ticket. To a connection with write access, and only where the integration acts on commit commands, they explain #comment and #<transition> too. Those commands run as the commit's author once pushed, whatever the connection may do, so a read-only connection is not told about them. Changes made that way carry the author's name only, not the agent's. If the integration runs a transition when a pull request is merged, the instructions name it.

Results and errors

A tool that succeeds returns the same data as the REST endpoint twice: as structuredContent, and as JSON text in content for clients that read only text.

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "content": [{"type": "text", "text": "{\"key\":\"WEB-142\",\"summary\":\"Checkout button misaligned on Safari\",…}"}],
    "structuredContent": {"key": "WEB-142", "summary": "Checkout button misaligned on Safari", …},
    "isError": false
  }
}

A tool that fails for a reason the model can act on (a name that matches nothing, a permission the member lacks, a sprint already running) returns a result with isError: true and the same sentence the REST API gives, so the model reads it and can try again:

{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "content": [{"type": "text", "text": "Type \"Bugg\" matches nothing; one of: Task, Bug, Story, Epic, Sub-task."}],
    "isError": true
  }
}

Protocol errors are JSON-RPC errors: -32601 for a method the server does not have, -32602 for an unknown tool or a call without a name, and -32600 for a body that is not a JSON-RPC 2.0 message.

Authentication is HTTP, before any JSON-RPC: a missing or invalid token is 401 with the WWW-Authenticate header that starts OAuth, 403 with the code feature_disabled while the MCP server is switched off, and 429 past the rate limit.

By hand

The whole protocol is a few POSTs, which makes it easy to try with curl:

curl https://your-workspace.albaticket.com/mcp \
  -H "Authorization: Bearer alba_pat_…" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'

curl https://your-workspace.albaticket.com/mcp \
  -H "Authorization: Bearer alba_pat_…" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_tickets","arguments":{"assignee":"me","status":"open"}}}'

ping is answered with an empty result. The MCP Inspector (npx @modelcontextprotocol/inspector) connects to the address too, with OAuth or a header.