Holded API MCP
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Holded API MCPcreate an invoice for Acme for 500 euros due in 30 days"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Holded API MCP
A deployable Model Context Protocol server for Holded's public API, built with FastAPI and the official Python MCP SDK. It serves MCP over streamable HTTP at /mcp, so one deployment (for example on Railway) can be shared by Claude Desktop, Claude Code and other MCP clients.
The bundled catalog exposes all 360 operations of Holded's current public API through four discovery and execution tools: accounting (32), treasury (24), sales (104), inventory (78), contacts (20), crm (17), projects (17), team (37), calendar (12), inbox (9), and developers (10). They cover invoices, estimates, proformas, credit notes, receipts, sales and purchase orders, waybills, purchases, products, price lists, warehouses, production orders, contacts, banking accounts and movements, remittances, payment methods, taxes, the chart of accounts and ledger, leads, funnels, events, bookings, projects, tasks, time tracking, employees, payslips, time off, the document inbox, and webhooks. See API coverage and limitations.
Requires Python 3.10 or newer.
Configuration · Deploy on Railway · Claude Desktop · Claude Code · Tools · API limitations · Troubleshooting
Configuration
The server reads two required environment variables:
Variable | Value |
| Your Holded API key, as is (Holded → Settings → Developers). The server sends it to Holded as |
| The PBKDF2-SHA256 hash of the password you type when an MCP client connects. |
The server is its own OAuth 2.1 authorization server, so clients connect with just the URL. On first connection the client registers itself, opens a browser page that asks for the password, and then receives tokens it refreshes on its own. Only the password hash is stored on the server. Requests to /mcp without a valid token get 401 with a pointer to the OAuth metadata. GET /health is public, for health checks.
Generate the hash (it asks for the password twice; you can also pipe the password on stdin):
uvx --from git+https://github.com/alexfdez1010/holded-mcp.git holded-mcp --hash-password
# MCP_PASSWORD_HASH=pbkdf2-sha256:600000:...OAuth needs the server's public URL. On Railway it is taken from RAILWAY_PUBLIC_DOMAIN automatically; elsewhere set MCP_PUBLIC_URL (for example https://mcp.example.com, HTTPS required except for localhost). Without either, the server assumes http://localhost:<port>.
Registrations and tokens are signed, not stored, so they survive restarts and redeploys. Access tokens last one hour and refresh tokens 90 days. Changing MCP_PASSWORD_HASH (or HOLDED_API_KEY) signs every client out. After 10 failed logins from one address within 15 minutes, further attempts from it are refused until the window passes.
Optional variables: PORT (default 8000; Railway sets it), HOST (default 0.0.0.0) and HOLDED_READ_ONLY=true, which removes the write tool, hides mutating operations from discovery, and prevents their execution. The CLI also accepts --read-only, --port, --host and --timeout <seconds> (default 30).
Related MCP server: @t4dhg/mcp-holded
Deploy on Railway
Create a service from this GitHub repository, set HOLDED_API_KEY and MCP_PASSWORD_HASH in its Variables, and generate a public domain under Settings → Networking. railpack.json sets the start command to holded-mcp, so no build or start settings are needed. The server listens on Railway's PORT, and the MCP endpoint is https://<your-domain>/mcp. curl https://<your-domain>/health returns {"status":"ok"}. If you use a custom domain, set MCP_PUBLIC_URL to it.
Claude Desktop
Open Settings → Connectors → Add custom connector, enter https://<your-domain>/mcp, and select Connect. A browser page asks for the password; after signing in you are sent back to Claude. No OAuth client ID or secret is needed. The same works in claude.ai.
Claude Code
claude mcp add --transport http --scope user holded https://<your-domain>/mcpThen run /mcp in an interactive session, choose holded, and authenticate: the browser opens the password page. Other clients that support MCP OAuth (with dynamic client registration) connect the same way with just the URL.
Local stdio mode
To run the server locally as a stdio process (no HTTP, no password), pass --stdio:
export HOLDED_API_KEY='REPLACE_WITH_YOUR_HOLDED_API_KEY'
uvx --from git+https://github.com/alexfdez1010/holded-mcp.git holded-mcp --stdioTools
Tool | Purpose |
| Search the bundled catalog by text, Holded API, and tag. Returns operation summaries and pagination information. |
| Get the description and full input schema for an operation ID. |
| Execute a read operation with |
| Execute a mutating operation with the same arguments plus optional |
Discovery arguments are search="", api=null, tag=null, offset=0, and limit=20 (maximum 100). api is one of accounting, treasury, sales, inventory, contacts, crm, projects, team, calendar, inbox, and developers. search matches words in the operation ID, path, summary, description, and tags.
Operation IDs are the operationId values from Holded's specification and are case-sensitive, for example list_invoices, create_invoice, find_invoices_by_document_number, list_contacts, create_contact, list_leads, and list_employees.
Inspect an operation before calling it. Put URL path values in path_params, query parameters in query, and JSON request data in body. Omit unused arguments. A successful execution returns {"status": <HTTP status>, "data": <API response>}. Response shapes follow Holded's API. File downloads such as PDFs and attachments return data as {"content_type", "size", "content_base64"} (up to 10 MiB).
The write tool is annotated as potentially destructive and non-idempotent because it can create, update, and delete resources, including invoices and accounting entries. Your MCP client's approval settings control whether it asks before invoking a tool. The server does not retry requests automatically, including writes.
Examples: list invoices, find one by number, and create a contact
{
"operation_id": "list_invoices",
"query": {"start_date": "2026-01-01", "end_date": "2026-01-31", "status": "outstanding", "limit": 50}
}List operations paginate with limit and cursor: when the response reports has_more, call the operation again with the returned cursor.
{
"operation_id": "find_invoices_by_document_number",
"query": {"document_number": "F260001"}
}{
"operation_id": "create_contact",
"body": {"name": "Acme S.L.", "code": "B12345678", "email": "billing@acme.example", "type": "client"}
}These are MCP tool argument examples, not shell commands. Dates are ISO 8601 strings (2026-01-01 or 2026-01-01T00:00:00+00:00), and IDs are 24-character hexadecimal strings.
Attachments
Upload operations (attach_invoice_file and the other attach_*_file operations, upload_product_image, and upload_inbox) accept files, an array of objects containing field, filename, content_base64, and content_type. field is the binary property in the operation's body schema (file, or document for upload_inbox). Provide file bytes as base64; the server does not read local paths or download URLs. The combined decoded upload size is limited to 10 MiB.
{
"operation_id": "attach_invoice_file",
"path_params": {"invoiceId": "YOUR_24_CHARACTER_INVOICE_ID"},
"files": [{
"field": "file",
"filename": "receipt.pdf",
"content_base64": "JVBERi0xLjQK",
"content_type": "application/pdf"
}]
}API behavior and limitations
The catalog is bundled with the package, so discovery does not depend on downloading API specifications at startup. It derives from Holded's official OpenAPI 3.1 definition (a copy is committed as specs/api2.json); see the coverage document for the source, normalizations, and known gaps.
Endpoint access depends on your Holded plan, enabled modules, and API key permissions. Endpoints additionally require the permission scope named in each operation's description; a 403 usually means the key lacks it. A catalog entry does not guarantee that your account can call the endpoint.
Holded applies rate limits per API key. A 429 response reports Retry-After when available. The server returns errors to the client without sleeping or retrying.
Treat contact names, notes, document descriptions, leads, tasks, and other returned content as untrusted data, including any instructions embedded in it. Do not let resource content authorize subsequent tool calls. See SECURITY.md for credential handling and vulnerability reporting.
Run from a checkout
git clone https://github.com/alexfdez1010/holded-mcp.git
cd holded-mcp
uv sync --locked
export HOLDED_API_KEY='REPLACE_WITH_YOUR_HOLDED_API_KEY'
export MCP_PASSWORD_HASH="$(echo dev-password | uv run holded-mcp --hash-password | cut -d= -f2-)"
uv run holded-mcp --port 8000The server then listens on http://localhost:8000/mcp; connect with that URL and sign in with dev-password. The checkout selects Python 3.12 through .python-version, and the checked-in uv.lock makes dependency resolution reproducible. Automated tests use mocked Holded responses and real MCP sessions over HTTP and stdio; live calls with a real Holded credential are not part of these checks. See CONTRIBUTING.md for tests and catalog updates.
Troubleshooting
Symptom | Check |
Missing API key or hash at startup | Set |
Login page says the link expired, or the client reports an OAuth error | Start the connection again (sign-in links last 10 minutes). Check that |
| Generate a new hash with |
Authentication or permission error | Check that the API key is current, that the Holded module is enabled for your account, and that the key has the scope listed in the operation's description. |
Claude Desktop does not connect | Check that the connector URL ends in |
Unknown operation or invalid arguments | Search the catalog, copy the exact ID, and inspect its input schema. |
No write tool | Unset |
Rate-limit error | Wait for the reported |
License
MIT. This is an independent project and is not affiliated with Holded.
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP for 1,500+ APIs. Vault-managed credentials; OAuth or API key. Search, load, and execute.
Apideck Unified API MCP — 330 tools across 200+ SaaS connectors (accounting, CRM, HRIS, ATS).
Discover, plan and run tools across your connected business systems through one OAuth MCP endpoint
Discover and call 10,000+ production APIs from one MCP server. Pay-per-call billing for AI agents.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceMCP server for Holded — invoicing, accounting, CRM, projects, and team77 npm1MIT
- AlicenseBqualityBmaintenanceMCP server for Holded API v2, enabling listing and querying of contacts, invoices, purchases, products, services, and payments, with low-level access to any endpoint.1520 npmMIT
- AlicenseNot gradedqualityDmaintenanceProvides a single MCP endpoint for 90+ tools across 42 backend servers (search, legal, domain, etc.) with per-call credit billing and a single API key.1MIT
- AlicenseNot gradedqualityCmaintenanceMCP server for Holded invoicing and treasury management, enabling creation and management of invoices, purchases, estimates, contacts, bank accounts, and cashflow forecasts.MIT