MCP server Max
Connect Claude, Cursor, and any other MCP-compatible AI tool to your Saku workspace. The Saku MCP server exposes boards, cards, team notes, and CRM as callable tools so agents can read, create, and update your work without leaving the AI chat. For REST details and permissions, see the API reference.
Published npm package: @florianindustries/saku-mcp (npm). Use npx -y @florianindustries/saku-mcp in your host config (see below).
How it works
Saku's hosted MCP endpoint uses Streamable HTTP at https://mcp.saku.ie/mcp. Connect it in any host that supports remote MCP servers and send your scoped key as a Bearer token. The endpoint lists its tools publicly for discovery, but requires the key before it can access workspace data.
The @florianindustries/saku-mcp npm package remains available as a local process that speaks the Model Context Protocol over stdio. Your AI host (Claude Desktop, Cursor, etc.) spawns it and routes tool calls through it to the Saku REST API. The server never stores your data; it forwards requests and returns responses.
Requirements
- Saku Max plan (API keys are a Max feature)
- An MCP-compatible host with Streamable HTTP support
- Node.js 20 or later only when using the local npm package
Setup
1. Add the hosted endpoint
In your host's remote MCP settings, add https://mcp.saku.ie/mcp with Streamable HTTP transport and an Authorization: Bearer YOUR_SAKU_API_KEY header.
2. Create an API key
Visit the authorisation page to create a scoped API key in your browser. You'll pick exactly which permissions to grant and which team to connect.
Authorise MCP access →Alternatively, go to Boards → Settings → API and create a key manually. Copy the key: it's shown only once.
3a. Claude Desktop with the local package
Open your Claude Desktop configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add the following, replacing the placeholder with your real key:
{
"mcpServers": {
"saku": {
"command": "npx",
"args": ["-y", "@florianindustries/saku-mcp"],
"env": {
"SAKU_API_KEY": "<your SAKU_API_KEY from .env>"
}
}
}
}Restart Claude Desktop. You should see a hammer icon: Saku tools are now available.
3b. Cursor with the local package
Open Cursor → Settings → MCP and add a new server with the following JSON:
{
"mcpServers": {
"saku": {
"command": "npx",
"args": ["-y", "@florianindustries/saku-mcp"],
"env": {
"SAKU_API_KEY": "<your SAKU_API_KEY from .env>"
}
}
}
}3c. Other local MCP hosts
Any host that supports the stdio transport can run Saku MCP. The command is:
SAKU_API_KEY=YOUR_SAKU_API_KEY npx -y @florianindustries/saku-mcp
Available tools
Tools are only available if the API key includes the required permission. Create a key with broader permissions later if needed (you'll need to create a new key; permissions can't be changed after creation).
list_boardsboards:readList all active boards with their lists, labels, and members. Start here to discover IDs.
get_boardboards:readGet a specific board with lists and cards (paginated per list). Each list includes cardsTotalCount for non-archived cards; use listCardLimit (default 200, max 500) and listCardOffset on the API or MCP tool to page.
| Argument | Type | Required |
|---|---|---|
| boardId | string | Yes |
| listCardLimit | number | No |
| listCardOffset | number | No |
create_cardcards:createCreate a new card in a list.
| Argument | Type | Required |
|---|---|---|
| listId | string | Yes |
| title | string | Yes |
| description | string | No |
| labels | string[] | No |
| assignees | string[] | No |
| source | string | No |
get_cardcards:readGet full card details including checklists, priority, due date, and assignees.
| Argument | Type | Required |
|---|---|---|
| cardId | string | Yes |
update_cardcards:updateUpdate a card's title, description, priority, or due date. Only send the fields you want to change.
| Argument | Type | Required |
|---|---|---|
| cardId | string | Yes |
| title | string | No |
| description | string | No |
| priority | CRITICAL | URGENT | HIGH | MEDIUM | LOW | MINOR | null | No |
| dueDate | ISO 8601 string | null | No |
archive_cardcards:deleteArchive a card (reversible in the app). Uses DELETE /api/v1/cards/:id.
| Argument | Type | Required |
|---|---|---|
| cardId | string | Yes |
create_listlists:createAdd a new list (column) to a board. The list is appended after existing lists.
| Argument | Type | Required |
|---|---|---|
| boardId | string | Yes |
| name | string | Yes |
list_membersmembers:readList all team members with their IDs, names, and emails (useful for resolving assignees). Uses GET /api/v1/members.
list_notesnotes:readList team-visible notes with pagination. Also returns writable team vaults; start here to discover vault IDs for create_note.
| Argument | Type | Required |
|---|---|---|
| vaultId | string | No |
| limit | number | No |
| offset | number | No |
get_notenotes:readGet a note by ID, including full contentMarkdown.
| Argument | Type | Required |
|---|---|---|
| noteId | string | Yes |
create_notenotes:createCreate a note in a team vault. Content is Markdown; wiki-links like [[note:ID]] are resolved automatically.
| Argument | Type | Required |
|---|---|---|
| vaultId | string | Yes |
| title | string | No |
| contentMarkdown | string | No |
update_notenotes:updateUpdate a note's title and/or content. Send only fields to change; contentMarkdown REPLACES the whole body (read-modify-write via get_note first).
| Argument | Type | Required |
|---|---|---|
| noteId | string | Yes |
| title | string | No |
| contentMarkdown | string | No |
list_crm_accountscrm:readList CRM accounts (companies) with cursor pagination. Use nextCursor from the previous response. Offset is rejected.
| Argument | Type | Required |
|---|---|---|
| limit | number | No |
| cursor | string | No |
get_crm_accountcrm:readGet a CRM account by ID, including its contacts.
| Argument | Type | Required |
|---|---|---|
| accountId | string | Yes |
create_crm_accountcrm:writeCreate a CRM account (company).
| Argument | Type | Required |
|---|---|---|
| companyName | string | Yes |
| primaryEmail | string | No |
| phone | string | No |
| website | string | No |
create_crm_contactcrm:writeCreate a CRM contact. Optionally link to an account via accountId (mapped to clientId in the API).
| Argument | Type | Required |
|---|---|---|
| name | string | Yes |
| accountId | string | No |
| string | No | |
| phone | string | No |
| jobTitle | string | No |
list_crm_contactscrm:readList CRM contacts with optional account filter and cursor pagination. accountId maps to clientId in the API. Offset is rejected.
| Argument | Type | Required |
|---|---|---|
| accountId | string | No |
| limit | number | No |
| cursor | string | No |
list_crm_stagescrm:readList lead and deal pipeline stages. Call before creating or moving leads/deals to discover stage IDs.
list_crm_pipelinecrm:readList leads or deals in the CRM pipeline with optional filters and cursor pagination. Offset is rejected.
| Argument | Type | Required |
|---|---|---|
| kind | "leads" | "deals" | Yes |
| stageId | string | No |
| accountId | string | No |
| status | string | No |
| limit | number | No |
| cursor | string | No |
create_crm_leadcrm:writeCreate a CRM lead. Use list_crm_stages for stage IDs if not providing stageId.
| Argument | Type | Required |
|---|---|---|
| title | string | Yes |
| description | string | No |
| source | string | No |
| stageId | string | No |
| accountId | string | No |
| contactId | string | No |
create_crm_dealcrm:writeCreate a CRM deal. Use list_crm_stages for stage IDs if not providing stageId.
| Argument | Type | Required |
|---|---|---|
| title | string | Yes |
| description | string | No |
| stageId | string | No |
| accountId | string | No |
| contactId | string | No |
| leadId | string | No |
| valueCents | number | No |
| closeDate | ISO 8601 string | No |
update_crm_leadcrm:writeUpdate a CRM lead. Only send fields to change; use list_crm_stages for stage IDs.
| Argument | Type | Required |
|---|---|---|
| leadId | string | Yes |
| title | string | No |
| description | string | No |
| source | string | No |
| stageId | string | No |
| status | OPEN | QUALIFIED | LOST | No |
update_crm_dealcrm:writeUpdate a CRM deal. Only send fields to change; use list_crm_stages for stage IDs.
| Argument | Type | Required |
|---|---|---|
| dealId | string | Yes |
| title | string | No |
| description | string | No |
| stageId | string | No |
| status | OPEN | WON | LOST | No |
| valueCents | number | No |
| closeDate | ISO 8601 string | null | No |
list_crm_communicationscrm:readList CRM communications for an account with optional filters and cursor pagination.
| Argument | Type | Required |
|---|---|---|
| accountId | string | Yes |
| limit | number | No |
| cursor | string | No |
| kind | EMAIL | CALL | MEETING | NOTE | OTHER | No |
create_crm_communicationcrm:writeLog a CRM communication on an account.
| Argument | Type | Required |
|---|---|---|
| accountId | string | Yes |
| kind | EMAIL | CALL | MEETING | NOTE | OTHER | Yes |
| subject | string | No |
| body | string | No |
| occurredAt | ISO 8601 string | No |
list_time_entriestime:readList your time entries with pagination.
| Argument | Type | Required |
|---|---|---|
| limit | number | No |
| offset | number | No |
create_time_entrytime:writeCreate a manual time entry. Provide startTime and endTime, or durationMinutes.
| Argument | Type | Required |
|---|---|---|
| cardId | string | No |
| description | string | No |
| startTime | ISO 8601 string | No |
| endTime | ISO 8601 string | No |
| durationMinutes | number | No |
| isBillable | boolean | null | No |
| tagNames | string[] | No |
start_timertime:writeStart a running timer. Any other active timer is stopped first (one active timer per person).
| Argument | Type | Required |
|---|---|---|
| cardId | string | No |
| description | string | No |
| isBillable | boolean | null | No |
| tagNames | string[] | No |
stop_timertime:writeStop your active timer, optionally rounding the duration.
| Argument | Type | Required |
|---|---|---|
| timeEntryId | string | No |
| cardId | string | No |
| description | string | No |
| roundingMinutes | 0 | 6 | 15 | No |
list_invoicesinvoices:readList invoices for the API key team with optional status/client filters and cursor pagination.
| Argument | Type | Required |
|---|---|---|
| clientId | string | No |
| cursor | string | No |
| limit | number | No |
| status | DRAFT | SENT | VIEWED | PAYMENT_PENDING | PAID | OVERDUE | VOID | No |
get_invoiceinvoices:readGet an invoice by ID including lines, payments, activity, and payUrl.
| Argument | Type | Required |
|---|---|---|
| invoiceId | string | Yes |
create_invoiceinvoices:writeCreate a draft invoice for a CRM client, optionally with manual line items. Money fields use integer cent strings. Currency defaults from Stripe Connect when omitted (EUR/USD/GBP).
| Argument | Type | Required |
|---|---|---|
| clientId | string | Yes |
| currency | EUR | USD | GBP | No |
| lines | array of { title, description?, quantity, unitCents (string), vatRatePercent? } | No |
send_invoiceinvoices:writeIssue (if needed) and email an invoice with PDF attachment. API key owner must be team OWNER or ADMIN.
| Argument | Type | Required |
|---|---|---|
| invoiceId | string | Yes |
| recipientEmails | string[] | No |
Permissions
API keys are scoped to specific permissions. Grant only what your use case needs.
| Permission | Label | Enables |
|---|---|---|
| boards:read | Read boards | list_boards, get_board |
| cards:read | Read cards | get_card |
| cards:create | Create cards | create_card |
| cards:update | Update cards | update_card |
| cards:delete | Delete cards | archive_card |
| lists:create | Create lists | create_list |
| members:read | Read members | list_members (GET /api/v1/members); team member emails are omitted from board responses without this scope |
| notes:read | Read notes | list_notes, get_note |
| notes:create | Create notes | create_note |
| notes:update | Update notes | update_note |
| crm:read | Read CRM | list_crm_accounts, get_crm_account, list_crm_contacts, list_crm_stages, list_crm_pipeline, list_crm_communications |
| crm:write | Write CRM | create_crm_account, create_crm_contact, create_crm_lead, create_crm_deal, update_crm_lead, update_crm_deal, create_crm_communication |
| crm:export | Export CRM | Export CRM graph (Max API keys; not granted to existing keys by default) |
| crm:import | Import CRM | Import CRM data (Max API keys) |
| crm:merge | Merge CRM | Merge CRM duplicates (Max API keys) |
| crm:forecast | CRM forecast | Pipeline forecast (Max) |
| crm:automation | CRM automation | CRM workflow automation (Max) |
| time:read | Read time | list_time_entries |
| time:write | Write time | create_time_entry, start_timer, stop_timer |
| expenses:read | Read expenses | REST expense reads and receipt metadata |
| expenses:write | Write expenses | REST expense creation, review, reimbursement, and invoicing |
| invoices:read | Read invoices | list_invoices, get_invoice |
| invoices:write | Write invoices | create_invoice, send_invoice |
Security
- Keys are team-scoped. One key = one team. Create separate keys for separate teams or tools.
- Permissions can't be changed after creation; create a new key if you need different scopes.
- Revoke a key any time in Boards → Settings → API. The MCP server will immediately start returning 401s.
- The server runs locally on your machine; your key and data never pass through a third-party relay.
Troubleshooting
- "SAKU_API_KEY is not set"
- The
SAKU_API_KEYenv var is missing from your host config. Check that"env"is set correctly in your config JSON. - "Unauthorized" errors
- The key may have been revoked or the value was copied incorrectly. Create a new key at /mcp/authorize and update your config.
- "Missing permission: …" errors
- The tool requires a permission not included in your key. Create a new key with the needed permissions; permissions can't be added to existing keys.
- Tools don't appear in the host UI
- Make sure Node.js 20+ is installed and accessible from your PATH. Restart the AI host after editing the config file.