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
POSTto/mcpwith a JSON-RPC 2.0 body and the bearer token. SendAccept: 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 Acceptedwith 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-Idis issued, and any server of a clustered workspace answers any request. GETandDELETEare answered405: 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.