MCP server
Use Alya Books from Claude, Cursor, ChatGPT or any MCP client: invoices, incoming documents, the ERP intake, customers, products, webhooks and VAT as tools.
Alya Books exposes its API as a Model Context Protocol server. Every tool maps onto one REST route, so validation, permissions and behaviour are identical to the API.
Endpoint
POST https://api.alyabooks.ai/api/v1/mcp
Transport: Streamable HTTP, stateless. Requests are JSON-RPC 2.0 messages (single or batched); responses are JSON. The server never opens a server-sent events stream, so GET /mcp answers 405.
Headers on every request:
Content-Type: application/json
Accept: application/json
X-API-KEY: sk_live_…
X-Organization-Id: <client id> (optional, CA firms)
Authentication is the same API key as the REST API; a tool is only callable when the key has its scope (invoices:write, incoming:read, webhooks:manage, …). Test keys make the assistant work on test documents.
Connect from claude.ai or ChatGPT (sign-in)
Chat apps that add MCP servers as custom connectors cannot send an API key, so the endpoint also supports OAuth 2.1, the way the MCP specification describes:
- In Claude (Settings → Connectors → Add custom connector) or ChatGPT (connectors in developer mode), enter the MCP URL:
https://<your API host>/functions/v1/api-v1/mcp(https://api.alyabooks.ai/api/v1/mcponce the API domain is live). - The app registers itself and opens Alya Books's sign-in. After signing in, you choose the organisation, Test or Live and the permissions, then click Allow.
- The app receives a one-hour access token and a refresh token; it refreshes on its own. Revoke it any time under API access → API keys → Connected apps.
What the server publishes, for client developers:
| Protected resource metadata | {API}/.well-known/oauth-protected-resource (also linked from the WWW-Authenticate header of every 401 from /mcp) |
| Authorization server metadata | {API}/.well-known/openid-configuration and {API}/.well-known/oauth-authorization-server; issuer {API} |
| Registration | POST {API}/oauth/register (RFC 7591), public clients only (token_endpoint_auth_method: none), https redirect URIs or http on localhost |
| Authorization | the Alya Books app's /oauth/authorize; authorization code with PKCE S256 required |
| Token | POST {API}/oauth/token: authorization_code and refresh_token; refresh tokens rotate on every use; codes are single-use and a reused code revokes what it produced |
| Revocation | POST {API}/oauth/revoke (RFC 7009) |
Access tokens (oat_…) work on /mcp only; the REST API keeps using API keys. A token belongs to one organisation, so X-Organization-Id cannot switch it. Clients that only look for discovery documents at the host's root, rather than under the API's path, need the API domain to forward /.well-known/… to the API.
Connect a client with an API key
{
"mcpServers": {
"alyabooks": {
"type": "http",
"url": "https://api.alyabooks.ai/api/v1/mcp",
"headers": { "X-API-KEY": "sk_live_…" }
}
}
}
{
"mcpServers": {
"alyabooks": {
"url": "https://api.alyabooks.ai/api/v1/mcp",
"headers": { "X-API-KEY": "sk_live_…" }
}
}
}
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("https://api.alyabooks.ai/api/v1/mcp"), {
requestInit: { headers: { "X-API-KEY": process.env.ALYABOOKS_API_KEY! } },
});
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
const { tools } = await client.listTools();
const result = await client.callTool({ name: "list_invoices", arguments: { status: "accepted", perPage: 5 } });
curl -X POST https://api.alyabooks.ai/api/v1/mcp -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_account", "arguments": {} } }'
Claude Code: claude mcp add --transport http alyabooks https://api.alyabooks.ai/api/v1/mcp --header "X-API-KEY: sk_live_…".
Tools
149 tools in 17 groups: 72 only read, the rest change data. Tool descriptions state their scope; tools/list returns full JSON Schemas for the arguments and the annotations below.
| Group | Tools |
|---|---|
| Invoice creation | create_tax_invoice, create_credit_note, create_debit_note, create_invoices_batch, replace_draft_invoice |
| Invoice management | list_invoices, get_invoice, get_invoice_status, get_invoice_validation, validate_invoice, submit_invoice, send_invoice, download_invoice, delete_draft_invoice, update_invoice_housekeeping, list_design_templates, list_invoice_payments, record_invoice_payment |
| Invoice numbers | get_next_invoice_number, verify_invoice_number, get_invoice_number_settings, update_invoice_number_settings |
| Incoming invoices | simulate_incoming_invoice, import_incoming_invoice, list_incoming_imports, list_incoming_invoices, get_import, get_import_status, get_import_validation, get_import_content, get_import_summary, get_import_line_items, get_import_seller, get_import_buyer, get_import_taxes, get_import_payment_terms, get_import_payment_means, get_import_delivery, get_import_attachments, get_import_source, review_incoming_invoice, update_incoming_invoice, record_incoming_payment, delete_import |
| Customers | list_customers, get_customer, create_customer, update_customer, delete_customer, get_next_customer_number, verify_customer_number, lookup_peppol_participant, verify_customer_peppol, list_contact_persons, add_contact_person, update_contact_person, delete_contact_person, list_contact_addresses, add_contact_address, update_contact_address, delete_contact_address, import_customers |
| Products | list_products, get_product, create_product, update_product, delete_product, check_product_code, get_product_price, list_bundles, get_bundle, create_bundle, update_bundle, delete_bundle, expand_bundle, list_price_lists, get_price_list, create_price_list, update_price_list, delete_price_list, import_products |
| Webhooks | list_webhooks, list_webhook_events, create_webhook, update_webhook, delete_webhook, test_webhook, list_webhook_deliveries, retry_webhook_delivery |
| Custom fields | list_custom_fields, create_custom_field, update_custom_field, delete_custom_field, list_field_templates, create_field_template, update_field_template, activate_field_template, delete_field_template |
| Projects | list_projects, get_project, create_project, update_project, delete_project, list_project_documents |
| Recurring invoices | list_recurring_invoices, get_recurring_invoice, create_recurring_invoice, update_recurring_invoice, activate_recurring_invoice, pause_recurring_invoice, cancel_recurring_invoice, run_recurring_invoice_now, list_recurring_runs, preview_recurring_invoice, make_invoice_recurring, delete_recurring_invoice |
| Settings | list_email_templates, save_email_template, get_options |
| VAT | list_tax_rates, create_tax_rate, update_tax_rate, list_vat_periods, get_vat_worksheet, list_vat_returns, save_vat_return, reopen_vat_return, check_period_date, export_accounting |
| ERP intake | dry_run_invoice, dry_run_erp_message, send_erp_message, list_erp_messages, get_erp_message, replay_erp_message, list_erp_connectors, update_erp_connector |
| Integrations | list_connectors, sync_connector |
| Payments | create_payment_link, list_payment_links, cancel_payment_link |
| Bank feeds | list_bank_transactions, match_bank_transaction, ignore_bank_transaction, import_bank_statement, sync_bank_feeds |
| Account | get_account, list_managed_organizations |
Marked destructive (destructiveHint, so clients ask before running them): submit_invoice, send_invoice, delete_draft_invoice, delete_import, delete_customer, delete_contact_person, delete_contact_address, delete_product, delete_bundle, delete_price_list, delete_webhook, delete_custom_field, delete_field_template, delete_project, delete_recurring_invoice, reopen_vat_return, send_erp_message, replay_erp_message. Submitting, sending and replaying reach the FTA or a buyer and cannot be undone; deletes remove data. The dry runs (dry_run_invoice, dry_run_erp_message) are marked read-only: they store nothing.
Test or live. Every tools/call result carries _meta: { "einvoicing/environment": "test" } or "live", from the key in use. With a test key nothing reaches the live service provider.
Not available to assistants, on purpose: API keys, team members, the service-provider connection, the company profile, and the adviser's sign-off on an ERP charge map. Those are set in the app by the people responsible.
Tool results contain the API response as text and as structuredContent; API errors are returned as tool results with isError: true and the usual { code, statusCode, message, extra } envelope, with each validation error's path, explanation and fix, so the assistant can correct its input.
Resources
| URI | Content |
|---|---|
einvoicing://reference/pint-ae-fields | PINT AE business terms with UBL paths and the API field that carries each |
einvoicing://reference/editor-fields | Optional field groups and their input keys |
einvoicing://reference/tax-categories | VAT categories, rates and UBL codes |
einvoicing://reference/statuses | Invoice, payment, review and incoming statuses; document kinds; transaction types |
einvoicing://reference/webhook-events | Event catalogue |
einvoicing://openapi.json | The OpenAPI document |
einvoicing://docs/llms.txt | Short integration guide for language models |
Example: a CargoWise message from a chat
"Here is a UniversalTransaction from our CargoWise UAT. Check it, tell me what fails, fix our charge map and send it."
The assistant calls dry_run_erp_message with the XML and reads the mapping notes and failed rules (for example "Charge PSC has no mapping"). It proposes the mapping, calls update_erp_connector after you agree, runs the dry run again until it is valid, then calls send_erp_message. With a live key, send_erp_message is marked destructive, so the client asks you before it runs. If the message had already failed in the log, replay_erp_message processes it again instead.
Example conversation
"Create a tax invoice for Buyer LLC (TRN 100000000000011) for 10 consulting hours at AED 500, due in 30 days, submit it and tell me when the FTA accepts it."
The assistant calls create_tax_invoice with submit: true, reads validation and submission from the result, then polls get_invoice_status until invoiceStatus is accepted or rejected. With a webhook on invoice.reported your own system learns about it at the same moment.
Limits
- Stateless: no sessions, no subscriptions, no server-initiated messages. Prompts are not provided (
prompts/listis empty). - Same rate limit as the API (600 requests per minute per key).
download_invoicewithformat: "pdf"returns a download link (url,expiresAt,filename) that works for 15 minutes without a key, so an assistant can hand the PDF to the user instead of a very long base64 string.delivery: "inline"returns the base64 instead, anddelivery: "link"gives a link for XML or HTML too. XML and HTML come back as text by default;get_import_sourcereturns binary files as base64 instructuredContent.