Files
mcp-mokocrm-api/src/index.ts
T
Jonathan Miller fa5cb994c9
Changelog Validation / Validate CHANGELOG.md (push) Has been cancelled
Build & Release / Build & Release Pipeline (push) Has been cancelled
Deploy to Demo Server (SFTP) / Verify Deployment Permission (push) Has been cancelled
Standards Compliance / Secret Scanning (push) Has been cancelled
Standards Compliance / License Header Validation (push) Has been cancelled
Standards Compliance / Repository Structure Validation (push) Has been cancelled
Standards Compliance / Coding Standards Check (push) Has been cancelled
Standards Compliance / Workflow Configuration Check (push) Has been cancelled
Standards Compliance / Documentation Quality Check (push) Has been cancelled
Standards Compliance / README Completeness Check (push) Has been cancelled
Standards Compliance / Git Repository Hygiene (push) Has been cancelled
Standards Compliance / Script Integrity Validation (push) Has been cancelled
Standards Compliance / Line Length Check (push) Has been cancelled
Standards Compliance / File Naming Standards (push) Has been cancelled
Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
CodeQL Security Scanning / Analyze (actions) (push) Has been cancelled
CodeQL Security Scanning / Analyze (javascript) (push) Has been cancelled
Standards Compliance / Version Consistency Check (push) Has been cancelled
Standards Compliance / Dead Code Detection (push) Has been cancelled
Standards Compliance / File Size Limits (push) Has been cancelled
Standards Compliance / Binary File Detection (push) Has been cancelled
Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
Standards Compliance / Code Duplication Detection (push) Has been cancelled
Standards Compliance / Broken Link Detection (push) Has been cancelled
Standards Compliance / Code Complexity Analysis (push) Has been cancelled
Standards Compliance / API Documentation Coverage (push) Has been cancelled
Standards Compliance / Accessibility Check (push) Has been cancelled
Standards Compliance / Performance Metrics (push) Has been cancelled
Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
Deploy to Demo Server (SFTP) / SFTP Deploy → Demo (push) Has been cancelled
CodeQL Security Scanning / Security Scan Summary (push) Has been cancelled
Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Standards Compliance / Repository Health Check (push) Has been cancelled
Sync Version from README / Propagate README version (push) Has been cancelled
Standards Compliance / Compliance Summary (push) Has been cancelled
feat(tools): expand to 89 tools — shipments, contracts, interventions, expense reports, tickets, agenda events, payments, documents, members, stock movements, contact CRUD, order lines, task CRUD with timespent, project update, dictionaries, bank transactions, category CRUD, user create
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 14:24:10 -05:00

1781 lines
66 KiB
TypeScript

#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: dolibarr-api-mcp.Server
* INGROUP: dolibarr-api-mcp
* REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
* PATH: /src/index.ts
* VERSION: 01.00.00
* BRIEF: MCP server entry point — registers all Dolibarr API tools
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { loadConfig, getConnection } from './config.js';
import { DolibarrClient } from './client.js';
import type { DolibarrConfig, ApiResponse } from './types.js';
let config: DolibarrConfig;
function clientFor(connection?: string): DolibarrClient {
return new DolibarrClient(getConnection(config, connection));
}
function formatResponse(res: ApiResponse): { content: Array<{ type: 'text'; text: string }> } {
if (res.status >= 400) {
const error_data = res.data;
let msg: string;
if (typeof error_data === 'object' && error_data !== null && 'error' in error_data) {
const err = error_data as { error: { code: number; message: string } };
msg = `${err.error.code}: ${err.error.message}`;
} else {
msg = `HTTP ${res.status}: ${JSON.stringify(res.data, null, 2)}`;
}
return { content: [{ type: 'text' as const, text: `Error: ${msg}` }] };
}
return {
content: [{ type: 'text' as const, text: JSON.stringify(res.data, null, 2) }],
};
}
const ConnectionParam = {
connection: z.string().optional().describe('Named connection from config (uses default if omitted)'),
};
const PaginationParams = {
limit: z.number().optional().describe('Max results (default 100)'),
page: z.number().optional().describe('Page number (0-based)'),
sortfield: z.string().optional().describe('Field to sort by'),
sortorder: z.enum(['ASC', 'DESC']).optional().describe('Sort direction'),
};
function paginationQuery(params: { limit?: number; page?: number; sortfield?: string; sortorder?: string }): Record<string, string> {
const q: Record<string, string> = {};
if (params.limit !== undefined) q['limit'] = String(params.limit);
if (params.page !== undefined) q['page'] = String(params.page);
if (params.sortfield) q['sortfield'] = params.sortfield;
if (params.sortorder) q['sortorder'] = params.sortorder;
return q;
}
type SqlOp = 'like' | '=' | '!=' | '<' | '>' | '<=' | '>=' | 'is' | 'isnot';
interface SqlFilterClause {
field: string;
op: SqlOp;
value: string | number | null;
}
function buildSqlFilter(clauses: SqlFilterClause[], join: 'AND' | 'OR' = 'AND'): string {
const parts = clauses.map(({ field, op, value }) => {
if (value === null) return `(${field}:${op}:null)`;
const v = typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : String(value);
return `(${field}:${op}:${v})`;
});
if (parts.length === 0) return '';
if (parts.length === 1) return parts[0];
return parts.join(` ${join} `);
}
function searchFilter(field: string, term: string): string {
return buildSqlFilter([{ field, op: 'like', value: `%${term}%` }]);
}
const server = new McpServer({
name: 'dolibarr-api-mcp',
version: '1.0.0',
});
// ── Third Parties (Customers/Suppliers) ─────────────────────────────────
server.tool(
'dolibarr_thirdparties_list',
'List third parties (customers, suppliers, prospects)',
{
mode: z.enum(['1', '2', '3', '4']).optional().describe('1=customer, 2=prospect, 3=supplier, 4=customer+supplier'),
search: z.string().optional().describe('Search in name'),
category: z.number().optional().describe('Filter by category ID'),
...PaginationParams,
...ConnectionParam,
},
async ({ mode, search, category, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (mode) params['mode'] = mode;
if (search) params['sqlfilters'] = searchFilter('t.nom', search);
if (category !== undefined) params['category'] = String(category);
return formatResponse(await client.get('/thirdparties', params));
},
);
server.tool(
'dolibarr_thirdparty_get',
'Get a single third party by ID',
{
id: z.number().describe('Third party ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/thirdparties/${id}`));
},
);
server.tool(
'dolibarr_thirdparty_create',
'Create a new third party',
{
name: z.string().describe('Company or individual name'),
client: z.enum(['0', '1', '2', '3']).optional().describe('0=neither, 1=customer, 2=prospect, 3=customer+prospect'),
fournisseur: z.enum(['0', '1']).optional().describe('0=not supplier, 1=supplier'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
address: z.string().optional().describe('Street address'),
zip: z.string().optional().describe('Postal code'),
town: z.string().optional().describe('City'),
country_id: z.number().optional().describe('Country ID'),
...ConnectionParam,
},
async ({ name, client: clientType, fournisseur, email, phone, address, zip, town, country_id, connection }) => {
const api = clientFor(connection);
const body: Record<string, unknown> = { name };
if (clientType !== undefined) body.client = clientType;
if (fournisseur !== undefined) body.fournisseur = fournisseur;
if (email) body.email = email;
if (phone) body.phone = phone;
if (address) body.address = address;
if (zip) body.zip = zip;
if (town) body.town = town;
if (country_id !== undefined) body.country_id = country_id;
return formatResponse(await api.post('/thirdparties', body));
},
);
server.tool(
'dolibarr_thirdparty_update',
'Update an existing third party',
{
id: z.number().describe('Third party ID'),
name: z.string().optional().describe('Company or individual name'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
address: z.string().optional().describe('Street address'),
zip: z.string().optional().describe('Postal code'),
town: z.string().optional().describe('City'),
country_id: z.number().optional().describe('Country ID'),
...ConnectionParam,
},
async ({ id, name, email, phone, address, zip, town, country_id, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = {};
if (name !== undefined) body.name = name;
if (email !== undefined) body.email = email;
if (phone !== undefined) body.phone = phone;
if (address !== undefined) body.address = address;
if (zip !== undefined) body.zip = zip;
if (town !== undefined) body.town = town;
if (country_id !== undefined) body.country_id = country_id;
return formatResponse(await client.put(`/thirdparties/${id}`, body));
},
);
server.tool(
'dolibarr_thirdparty_delete',
'Delete a third party',
{
id: z.number().describe('Third party ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.delete(`/thirdparties/${id}`));
},
);
// ── Invoices ────────────────────────────────────────────────────────────
server.tool(
'dolibarr_invoices_list',
'List invoices',
{
status: z.enum(['draft', 'unpaid', 'paid', 'cancelled']).optional().describe('Filter by invoice status'),
thirdparty_ids: z.string().optional().describe('Comma-separated third party IDs'),
search: z.string().optional().describe('Search in ref or ref_client'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, thirdparty_ids, search, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (thirdparty_ids) params['thirdparty_ids'] = thirdparty_ids;
if (search) params['sqlfilters'] = searchFilter('t.ref', search);
return formatResponse(await client.get('/invoices', params));
},
);
server.tool(
'dolibarr_invoice_get',
'Get a single invoice by ID',
{
id: z.number().describe('Invoice ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/invoices/${id}`));
},
);
server.tool(
'dolibarr_invoice_create',
'Create a new invoice',
{
socid: z.number().describe('Third party (customer) ID'),
type: z.enum(['0', '1', '2', '3']).optional().describe('0=standard, 1=replacement, 2=credit note, 3=deposit'),
date: z.string().optional().describe('Invoice date (YYYY-MM-DD or Unix timestamp)'),
note_public: z.string().optional().describe('Public note'),
note_private: z.string().optional().describe('Private note'),
...ConnectionParam,
},
async ({ socid, type, date, note_public, note_private, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { socid };
if (type !== undefined) body.type = type;
if (date) body.date = date;
if (note_public) body.note_public = note_public;
if (note_private) body.note_private = note_private;
return formatResponse(await client.post('/invoices', body));
},
);
server.tool(
'dolibarr_invoice_add_line',
'Add a line to an invoice',
{
id: z.number().describe('Invoice ID'),
desc: z.string().describe('Line description'),
subprice: z.number().describe('Unit price (HT)'),
qty: z.number().describe('Quantity'),
tva_tx: z.number().optional().describe('VAT rate (e.g. 20.0)'),
product_id: z.number().optional().describe('Product/service ID'),
...ConnectionParam,
},
async ({ id, desc, subprice, qty, tva_tx, product_id, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { desc, subprice, qty };
if (tva_tx !== undefined) body.tva_tx = tva_tx;
if (product_id !== undefined) body.fk_product = product_id;
return formatResponse(await client.post(`/invoices/${id}/lines`, body));
},
);
server.tool(
'dolibarr_invoice_validate',
'Validate (finalize) a draft invoice',
{
id: z.number().describe('Invoice ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.post(`/invoices/${id}/validate`, {}));
},
);
server.tool(
'dolibarr_invoice_set_paid',
'Mark an invoice as paid',
{
id: z.number().describe('Invoice ID'),
close_code: z.string().optional().describe('Close code (e.g. "bankorder", "cash")'),
close_note: z.string().optional().describe('Close note'),
...ConnectionParam,
},
async ({ id, close_code, close_note, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = {};
if (close_code) body.close_code = close_code;
if (close_note) body.close_note = close_note;
return formatResponse(await client.post(`/invoices/${id}/settopaid`, body));
},
);
// ── Proposals (Quotes) ─────────────────────────────────────────────────
server.tool(
'dolibarr_proposals_list',
'List commercial proposals (quotes)',
{
status: z.enum(['0', '1', '2', '3', '4']).optional().describe('0=draft, 1=validated, 2=signed, 3=not-signed, 4=billed'),
thirdparty_ids: z.string().optional().describe('Comma-separated third party IDs'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, thirdparty_ids, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (thirdparty_ids) params['thirdparty_ids'] = thirdparty_ids;
return formatResponse(await client.get('/proposals', params));
},
);
server.tool(
'dolibarr_proposal_get',
'Get a single proposal by ID',
{
id: z.number().describe('Proposal ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/proposals/${id}`));
},
);
server.tool(
'dolibarr_proposal_create',
'Create a new commercial proposal',
{
socid: z.number().describe('Third party (customer) ID'),
date: z.string().optional().describe('Proposal date (YYYY-MM-DD or Unix timestamp)'),
duree_validite: z.number().optional().describe('Validity duration in days'),
note_public: z.string().optional().describe('Public note'),
note_private: z.string().optional().describe('Private note'),
...ConnectionParam,
},
async ({ socid, date, duree_validite, note_public, note_private, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { socid };
if (date) body.date = date;
if (duree_validite !== undefined) body.duree_validite = duree_validite;
if (note_public) body.note_public = note_public;
if (note_private) body.note_private = note_private;
return formatResponse(await client.post('/proposals', body));
},
);
server.tool(
'dolibarr_proposal_add_line',
'Add a line to a proposal',
{
id: z.number().describe('Proposal ID'),
desc: z.string().describe('Line description'),
subprice: z.number().describe('Unit price (HT)'),
qty: z.number().describe('Quantity'),
tva_tx: z.number().optional().describe('VAT rate (e.g. 20.0)'),
product_id: z.number().optional().describe('Product/service ID'),
...ConnectionParam,
},
async ({ id, desc, subprice, qty, tva_tx, product_id, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { desc, subprice, qty };
if (tva_tx !== undefined) body.tva_tx = tva_tx;
if (product_id !== undefined) body.fk_product = product_id;
return formatResponse(await client.post(`/proposals/${id}/lines`, body));
},
);
server.tool(
'dolibarr_proposal_validate',
'Validate a draft proposal',
{
id: z.number().describe('Proposal ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.post(`/proposals/${id}/validate`, {}));
},
);
server.tool(
'dolibarr_proposal_close',
'Close a proposal (sign or refuse)',
{
id: z.number().describe('Proposal ID'),
status: z.enum(['2', '3']).describe('2=signed, 3=not signed (refused)'),
note: z.string().optional().describe('Close note'),
...ConnectionParam,
},
async ({ id, status, note, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { status: Number(status) };
if (note) body.note_private = note;
return formatResponse(await client.post(`/proposals/${id}/close`, body));
},
);
// ── Orders ──────────────────────────────────────────────────────────────
server.tool(
'dolibarr_orders_list',
'List customer orders',
{
status: z.enum(['0', '1', '2', '3', '-1']).optional().describe('0=draft, 1=validated, 2=processing, 3=delivered, -1=cancelled'),
thirdparty_ids: z.string().optional().describe('Comma-separated third party IDs'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, thirdparty_ids, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (thirdparty_ids) params['thirdparty_ids'] = thirdparty_ids;
return formatResponse(await client.get('/orders', params));
},
);
server.tool(
'dolibarr_order_get',
'Get a single order by ID',
{
id: z.number().describe('Order ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/orders/${id}`));
},
);
server.tool(
'dolibarr_order_create',
'Create a new customer order',
{
socid: z.number().describe('Third party (customer) ID'),
date: z.string().optional().describe('Order date (YYYY-MM-DD or Unix timestamp)'),
note_public: z.string().optional().describe('Public note'),
note_private: z.string().optional().describe('Private note'),
...ConnectionParam,
},
async ({ socid, date, note_public, note_private, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { socid };
if (date) body.date = date;
if (note_public) body.note_public = note_public;
if (note_private) body.note_private = note_private;
return formatResponse(await client.post('/orders', body));
},
);
server.tool(
'dolibarr_order_validate',
'Validate a draft order',
{
id: z.number().describe('Order ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.post(`/orders/${id}/validate`, {}));
},
);
// ── Products / Services ─────────────────────────────────────────────────
server.tool(
'dolibarr_products_list',
'List products and/or services',
{
type: z.enum(['0', '1']).optional().describe('0=product, 1=service'),
search: z.string().optional().describe('Search in label or ref'),
category: z.number().optional().describe('Filter by category ID'),
to_sell: z.enum(['0', '1']).optional().describe('1=for sale only'),
to_buy: z.enum(['0', '1']).optional().describe('1=for purchase only'),
...PaginationParams,
...ConnectionParam,
},
async ({ type, search, category, to_sell, to_buy, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (type) params['type'] = type;
if (search) params['sqlfilters'] = searchFilter('t.label', search);
if (category !== undefined) params['category'] = String(category);
if (to_sell) params['to_sell'] = to_sell;
if (to_buy) params['to_buy'] = to_buy;
return formatResponse(await client.get('/products', params));
},
);
server.tool(
'dolibarr_product_get',
'Get a single product/service by ID',
{
id: z.number().describe('Product ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/products/${id}`));
},
);
server.tool(
'dolibarr_product_create',
'Create a new product or service',
{
ref: z.string().describe('Product reference code'),
label: z.string().describe('Product label/name'),
type: z.enum(['0', '1']).optional().describe('0=product, 1=service (default 0)'),
price: z.number().optional().describe('Selling price (HT)'),
price_ttc: z.number().optional().describe('Selling price (TTC)'),
tva_tx: z.number().optional().describe('Default VAT rate'),
description: z.string().optional().describe('Description'),
status: z.enum(['0', '1']).optional().describe('1=on sale, 0=not on sale'),
status_buy: z.enum(['0', '1']).optional().describe('1=on purchase, 0=not on purchase'),
...ConnectionParam,
},
async ({ ref, label, type, price, price_ttc, tva_tx, description, status, status_buy, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { ref, label };
if (type !== undefined) body.type = type;
if (price !== undefined) body.price = price;
if (price_ttc !== undefined) body.price_ttc = price_ttc;
if (tva_tx !== undefined) body.tva_tx = tva_tx;
if (description) body.description = description;
if (status !== undefined) body.status = status;
if (status_buy !== undefined) body.status_buy = status_buy;
return formatResponse(await client.post('/products', body));
},
);
server.tool(
'dolibarr_product_update',
'Update a product or service',
{
id: z.number().describe('Product ID'),
ref: z.string().optional().describe('Product reference code'),
label: z.string().optional().describe('Product label/name'),
price: z.number().optional().describe('Selling price (HT)'),
description: z.string().optional().describe('Description'),
status: z.enum(['0', '1']).optional().describe('1=on sale, 0=not on sale'),
status_buy: z.enum(['0', '1']).optional().describe('1=on purchase, 0=not on purchase'),
...ConnectionParam,
},
async ({ id, ref, label, price, description, status, status_buy, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = {};
if (ref !== undefined) body.ref = ref;
if (label !== undefined) body.label = label;
if (price !== undefined) body.price = price;
if (description !== undefined) body.description = description;
if (status !== undefined) body.status = status;
if (status_buy !== undefined) body.status_buy = status_buy;
return formatResponse(await client.put(`/products/${id}`, body));
},
);
// ── Contacts / Addresses ────────────────────────────────────────────────
server.tool(
'dolibarr_contacts_list',
'List contacts/addresses',
{
search: z.string().optional().describe('Search in name'),
thirdparty_id: z.number().optional().describe('Filter by third party ID'),
...PaginationParams,
...ConnectionParam,
},
async ({ search, thirdparty_id, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (search) params['sqlfilters'] = searchFilter('t.lastname', search);
if (thirdparty_id !== undefined) params['thirdparty_ids'] = String(thirdparty_id);
return formatResponse(await client.get('/contacts', params));
},
);
server.tool(
'dolibarr_contact_get',
'Get a single contact by ID',
{
id: z.number().describe('Contact ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/contacts/${id}`));
},
);
// ── Projects ────────────────────────────────────────────────────────────
server.tool(
'dolibarr_projects_list',
'List projects',
{
status: z.enum(['0', '1', '2']).optional().describe('0=draft, 1=open, 2=closed'),
search: z.string().optional().describe('Search in title or ref'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, search, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
const projectClauses: SqlFilterClause[] = [];
if (status) projectClauses.push({ field: 't.fk_statut', op: '=', value: Number(status) });
if (search) projectClauses.push({ field: 't.title', op: 'like', value: `%${search}%` });
if (projectClauses.length) params['sqlfilters'] = buildSqlFilter(projectClauses);
return formatResponse(await client.get('/projects', params));
},
);
server.tool(
'dolibarr_project_get',
'Get a single project by ID',
{
id: z.number().describe('Project ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/projects/${id}`));
},
);
server.tool(
'dolibarr_project_create',
'Create a new project',
{
ref: z.string().describe('Project reference'),
title: z.string().describe('Project title'),
socid: z.number().optional().describe('Third party ID'),
description: z.string().optional().describe('Project description'),
date_start: z.string().optional().describe('Start date (YYYY-MM-DD or Unix timestamp)'),
date_end: z.string().optional().describe('End date (YYYY-MM-DD or Unix timestamp)'),
...ConnectionParam,
},
async ({ ref, title, socid, description, date_start, date_end, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { ref, title };
if (socid !== undefined) body.socid = socid;
if (description) body.description = description;
if (date_start) body.date_start = date_start;
if (date_end) body.date_end = date_end;
return formatResponse(await client.post('/projects', body));
},
);
// ── Tasks ───────────────────────────────────────────────────────────────
server.tool(
'dolibarr_tasks_list',
'List project tasks',
{
project_id: z.number().optional().describe('Filter by project ID'),
...PaginationParams,
...ConnectionParam,
},
async ({ project_id, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (project_id !== undefined) params['sqlfilters'] = buildSqlFilter([{ field: 't.fk_projet', op: '=', value: project_id }]);
return formatResponse(await client.get('/tasks', params));
},
);
server.tool(
'dolibarr_task_get',
'Get a single task by ID',
{
id: z.number().describe('Task ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/tasks/${id}`));
},
);
// ── Users ───────────────────────────────────────────────────────────────
server.tool(
'dolibarr_users_list',
'List Dolibarr users',
{
search: z.string().optional().describe('Search in name/login'),
...PaginationParams,
...ConnectionParam,
},
async ({ search, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (search) params['sqlfilters'] = searchFilter('t.login', search);
return formatResponse(await client.get('/users', params));
},
);
server.tool(
'dolibarr_user_get',
'Get a single user by ID',
{
id: z.number().describe('User ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/users/${id}`));
},
);
// ── Categories ──────────────────────────────────────────────────────────
server.tool(
'dolibarr_categories_list',
'List categories',
{
type: z.enum(['product', 'supplier', 'customer', 'member', 'contact', 'project']).optional().describe('Category type'),
search: z.string().optional().describe('Search in label'),
...PaginationParams,
...ConnectionParam,
},
async ({ type, search, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (type) params['type'] = type;
if (search) params['sqlfilters'] = searchFilter('t.label', search);
return formatResponse(await client.get('/categories', params));
},
);
// ── Bank Accounts ───────────────────────────────────────────────────────
server.tool(
'dolibarr_bankaccounts_list',
'List bank accounts',
{
...PaginationParams,
...ConnectionParam,
},
async ({ limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params = paginationQuery({ limit, page, sortfield, sortorder });
return formatResponse(await client.get('/bankaccounts', params));
},
);
// ── Supplier Invoices ───────────────────────────────────────────────────
server.tool(
'dolibarr_supplier_invoices_list',
'List supplier invoices',
{
status: z.enum(['draft', 'unpaid', 'paid', 'cancelled']).optional().describe('Filter by status'),
thirdparty_ids: z.string().optional().describe('Comma-separated supplier IDs'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, thirdparty_ids, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (thirdparty_ids) params['thirdparty_ids'] = thirdparty_ids;
return formatResponse(await client.get('/supplierinvoices', params));
},
);
// ── Supplier Orders ─────────────────────────────────────────────────────
server.tool(
'dolibarr_supplier_orders_list',
'List supplier orders (purchase orders)',
{
status: z.enum(['0', '1', '2', '3', '4', '5', '9']).optional().describe('0=draft, 1=validated, 2=approved, 3=ordered, 4=partially received, 5=received, 9=cancelled'),
thirdparty_ids: z.string().optional().describe('Comma-separated supplier IDs'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, thirdparty_ids, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (thirdparty_ids) params['thirdparty_ids'] = thirdparty_ids;
return formatResponse(await client.get('/supplierorders', params));
},
);
// ── Warehouses / Stock ──────────────────────────────────────────────────
server.tool(
'dolibarr_warehouses_list',
'List warehouses',
{
...PaginationParams,
...ConnectionParam,
},
async ({ limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params = paginationQuery({ limit, page, sortfield, sortorder });
return formatResponse(await client.get('/warehouses', params));
},
);
server.tool(
'dolibarr_product_stock',
'Get stock levels for a product across warehouses',
{
id: z.number().describe('Product ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/products/${id}/stock`));
},
);
// ── Shipments ───────────────────────────────────────────────────────────
server.tool(
'dolibarr_shipments_list',
'List shipments (expeditions)',
{
status: z.enum(['0', '1', '2']).optional().describe('0=draft, 1=validated, 2=closed'),
thirdparty_ids: z.string().optional().describe('Comma-separated third party IDs'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, thirdparty_ids, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (thirdparty_ids) params['thirdparty_ids'] = thirdparty_ids;
return formatResponse(await client.get('/shipments', params));
},
);
server.tool(
'dolibarr_shipment_get',
'Get a single shipment by ID',
{
id: z.number().describe('Shipment ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/shipments/${id}`));
},
);
server.tool(
'dolibarr_shipment_create',
'Create a new shipment',
{
socid: z.number().describe('Third party (customer) ID'),
origin_id: z.number().optional().describe('Source order ID'),
origin_type: z.string().optional().describe('Source type (e.g. "commande")'),
date_delivery: z.string().optional().describe('Delivery date (YYYY-MM-DD or Unix timestamp)'),
note_public: z.string().optional().describe('Public note'),
note_private: z.string().optional().describe('Private note'),
...ConnectionParam,
},
async ({ socid, origin_id, origin_type, date_delivery, note_public, note_private, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { socid };
if (origin_id !== undefined) body.origin_id = origin_id;
if (origin_type) body.origin_type = origin_type;
if (date_delivery) body.date_delivery = date_delivery;
if (note_public) body.note_public = note_public;
if (note_private) body.note_private = note_private;
return formatResponse(await client.post('/shipments', body));
},
);
server.tool(
'dolibarr_shipment_validate',
'Validate a draft shipment',
{
id: z.number().describe('Shipment ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.post(`/shipments/${id}/validate`, {}));
},
);
server.tool(
'dolibarr_shipment_close',
'Close a shipment (mark as delivered)',
{
id: z.number().describe('Shipment ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.post(`/shipments/${id}/close`, {}));
},
);
// ── Contracts / Subscriptions ───────────────────────────────────────────
server.tool(
'dolibarr_contracts_list',
'List contracts/subscriptions',
{
status: z.enum(['0', '1', '4', '5']).optional().describe('0=draft, 1=validated, 4=closed, 5=running'),
thirdparty_ids: z.string().optional().describe('Comma-separated third party IDs'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, thirdparty_ids, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (thirdparty_ids) params['thirdparty_ids'] = thirdparty_ids;
return formatResponse(await client.get('/contracts', params));
},
);
server.tool(
'dolibarr_contract_get',
'Get a single contract by ID',
{
id: z.number().describe('Contract ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/contracts/${id}`));
},
);
server.tool(
'dolibarr_contract_create',
'Create a new contract',
{
socid: z.number().describe('Third party ID'),
ref: z.string().optional().describe('Contract reference'),
date_contrat: z.string().optional().describe('Contract date (YYYY-MM-DD or Unix timestamp)'),
note_public: z.string().optional().describe('Public note'),
note_private: z.string().optional().describe('Private note'),
...ConnectionParam,
},
async ({ socid, ref, date_contrat, note_public, note_private, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { socid };
if (ref) body.ref = ref;
if (date_contrat) body.date_contrat = date_contrat;
if (note_public) body.note_public = note_public;
if (note_private) body.note_private = note_private;
return formatResponse(await client.post('/contracts', body));
},
);
server.tool(
'dolibarr_contract_validate',
'Validate a draft contract',
{
id: z.number().describe('Contract ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.post(`/contracts/${id}/validate`, {}));
},
);
// ── Interventions ───────────────────────────────────────────────────────
server.tool(
'dolibarr_interventions_list',
'List interventions (field service)',
{
status: z.enum(['0', '1', '2']).optional().describe('0=draft, 1=validated, 2=billed'),
thirdparty_ids: z.string().optional().describe('Comma-separated third party IDs'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, thirdparty_ids, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (thirdparty_ids) params['thirdparty_ids'] = thirdparty_ids;
return formatResponse(await client.get('/interventions', params));
},
);
server.tool(
'dolibarr_intervention_get',
'Get a single intervention by ID',
{
id: z.number().describe('Intervention ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/interventions/${id}`));
},
);
// ── Expense Reports ─────────────────────────────────────────────────────
server.tool(
'dolibarr_expensereports_list',
'List expense reports',
{
status: z.enum(['0', '2', '4', '5', '6', '99']).optional().describe('0=draft, 2=validated, 4=cancelled, 5=approved, 6=paid, 99=refused'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
return formatResponse(await client.get('/expensereports', params));
},
);
server.tool(
'dolibarr_expensereport_get',
'Get a single expense report by ID',
{
id: z.number().describe('Expense report ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/expensereports/${id}`));
},
);
server.tool(
'dolibarr_expensereport_create',
'Create a new expense report',
{
fk_user_author: z.number().describe('User ID of the author'),
date_debut: z.string().describe('Start date (YYYY-MM-DD or Unix timestamp)'),
date_fin: z.string().describe('End date (YYYY-MM-DD or Unix timestamp)'),
note_public: z.string().optional().describe('Public note'),
note_private: z.string().optional().describe('Private note'),
...ConnectionParam,
},
async ({ fk_user_author, date_debut, date_fin, note_public, note_private, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { fk_user_author, date_debut, date_fin };
if (note_public) body.note_public = note_public;
if (note_private) body.note_private = note_private;
return formatResponse(await client.post('/expensereports', body));
},
);
// ── Tickets (Helpdesk) ─────────────────────────────────────────────────
server.tool(
'dolibarr_tickets_list',
'List helpdesk tickets',
{
status: z.string().optional().describe('Filter by status'),
search: z.string().optional().describe('Search in subject'),
...PaginationParams,
...ConnectionParam,
},
async ({ status, search, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (status) params['status'] = status;
if (search) params['sqlfilters'] = searchFilter('t.subject', search);
return formatResponse(await client.get('/tickets', params));
},
);
server.tool(
'dolibarr_ticket_get',
'Get a single ticket by ID',
{
id: z.number().describe('Ticket ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/tickets/${id}`));
},
);
server.tool(
'dolibarr_ticket_create',
'Create a new helpdesk ticket',
{
subject: z.string().describe('Ticket subject'),
message: z.string().describe('Ticket message/description'),
type_code: z.string().optional().describe('Ticket type code'),
category_code: z.string().optional().describe('Ticket category code'),
severity_code: z.string().optional().describe('Ticket severity code'),
socid: z.number().optional().describe('Third party ID'),
notify_tiers_at_create: z.number().optional().describe('1=notify third party on creation'),
...ConnectionParam,
},
async ({ subject, message, type_code, category_code, severity_code, socid, notify_tiers_at_create, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { subject, message };
if (type_code) body.type_code = type_code;
if (category_code) body.category_code = category_code;
if (severity_code) body.severity_code = severity_code;
if (socid !== undefined) body.socid = socid;
if (notify_tiers_at_create !== undefined) body.notify_tiers_at_create = notify_tiers_at_create;
return formatResponse(await client.post('/tickets', body));
},
);
// ── Agenda / Events ─────────────────────────────────────────────────────
server.tool(
'dolibarr_agendaevents_list',
'List agenda events',
{
type: z.string().optional().describe('Event type code'),
status: z.enum(['-1', '0', '50', '100']).optional().describe('-1=cancelled, 0=draft, 50=in progress, 100=done'),
userassigned: z.number().optional().describe('Filter by assigned user ID'),
...PaginationParams,
...ConnectionParam,
},
async ({ type, status, userassigned, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (type) params['type'] = type;
if (status) params['status'] = status;
if (userassigned !== undefined) params['userassigned'] = String(userassigned);
return formatResponse(await client.get('/agendaevents', params));
},
);
server.tool(
'dolibarr_agendaevent_get',
'Get a single agenda event by ID',
{
id: z.number().describe('Event ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/agendaevents/${id}`));
},
);
server.tool(
'dolibarr_agendaevent_create',
'Create a new agenda event',
{
label: z.string().describe('Event label/title'),
type_code: z.string().describe('Event type code (e.g. "AC_RDV", "AC_TEL", "AC_OTH")'),
datep: z.string().describe('Start date (YYYY-MM-DD HH:MM:SS or Unix timestamp)'),
datef: z.string().optional().describe('End date'),
socid: z.number().optional().describe('Third party ID'),
contactid: z.number().optional().describe('Contact ID'),
fk_project: z.number().optional().describe('Project ID'),
userownerid: z.number().optional().describe('Owner user ID'),
note: z.string().optional().describe('Event note'),
percentage: z.enum(['-1', '0', '50', '100']).optional().describe('-1=N/A, 0=todo, 50=in progress, 100=done'),
...ConnectionParam,
},
async ({ label, type_code, datep, datef, socid, contactid, fk_project, userownerid, note, percentage, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { label, type_code, datep };
if (datef) body.datef = datef;
if (socid !== undefined) body.socid = socid;
if (contactid !== undefined) body.contactid = contactid;
if (fk_project !== undefined) body.fk_project = fk_project;
if (userownerid !== undefined) body.userownerid = userownerid;
if (note) body.note = note;
if (percentage) body.percentage = percentage;
return formatResponse(await client.post('/agendaevents', body));
},
);
server.tool(
'dolibarr_agendaevent_update',
'Update an agenda event',
{
id: z.number().describe('Event ID'),
label: z.string().optional().describe('Event label'),
datep: z.string().optional().describe('Start date'),
datef: z.string().optional().describe('End date'),
percentage: z.enum(['-1', '0', '50', '100']).optional().describe('Completion: -1=N/A, 0=todo, 50=in progress, 100=done'),
note: z.string().optional().describe('Event note'),
...ConnectionParam,
},
async ({ id, label, datep, datef, percentage, note, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = {};
if (label !== undefined) body.label = label;
if (datep !== undefined) body.datep = datep;
if (datef !== undefined) body.datef = datef;
if (percentage !== undefined) body.percentage = percentage;
if (note !== undefined) body.note = note;
return formatResponse(await client.put(`/agendaevents/${id}`, body));
},
);
// ── Payments ────────────────────────────────────────────────────────────
server.tool(
'dolibarr_invoice_payments',
'List payments for an invoice',
{
id: z.number().describe('Invoice ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/invoices/${id}/payments`));
},
);
server.tool(
'dolibarr_invoice_add_payment',
'Add a payment to an invoice',
{
id: z.number().describe('Invoice ID'),
datepaye: z.string().describe('Payment date (YYYY-MM-DD or Unix timestamp)'),
paymentid: z.number().describe('Payment type ID (e.g. 4=bank transfer, 6=credit card)'),
closepaidinvoices: z.string().optional().describe('"yes" to auto-close fully paid invoices'),
accountid: z.number().optional().describe('Bank account ID'),
num_payment: z.string().optional().describe('Payment number/reference'),
comment: z.string().optional().describe('Payment comment'),
...ConnectionParam,
},
async ({ id, datepaye, paymentid, closepaidinvoices, accountid, num_payment, comment, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { datepaye, paymentid };
if (closepaidinvoices) body.closepaidinvoices = closepaidinvoices;
if (accountid !== undefined) body.accountid = accountid;
if (num_payment) body.num_payment = num_payment;
if (comment) body.comment = comment;
return formatResponse(await client.post(`/invoices/${id}/payments`, body));
},
);
// ── Documents ───────────────────────────────────────────────────────────
server.tool(
'dolibarr_documents_list',
'List documents attached to an object',
{
modulepart: z.string().describe('Module name (e.g. "facture", "propal", "commande", "societe", "project")'),
id: z.number().optional().describe('Object ID'),
ref: z.string().optional().describe('Object ref (alternative to ID)'),
...ConnectionParam,
},
async ({ modulepart, id, ref, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { modulepart };
if (id !== undefined) params['id'] = String(id);
if (ref) params['ref'] = ref;
return formatResponse(await client.get('/documents', params));
},
);
server.tool(
'dolibarr_document_download',
'Download/get content of a document',
{
modulepart: z.string().describe('Module name (e.g. "facture", "propal", "commande")'),
original_file: z.string().describe('Relative file path (e.g. "FA2301-0001/FA2301-0001.pdf")'),
...ConnectionParam,
},
async ({ modulepart, original_file, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get('/documents/download', { modulepart, original_file }));
},
);
server.tool(
'dolibarr_document_builddoc',
'Generate/build a document (e.g. PDF invoice)',
{
modulepart: z.string().describe('Module name (e.g. "facture", "propal", "commande")'),
original_file: z.string().describe('Expected output filename'),
doctemplate: z.string().optional().describe('Document template name (e.g. "crabe", "sponge")'),
langcode: z.string().optional().describe('Language code (e.g. "en_US", "fr_FR")'),
...ConnectionParam,
},
async ({ modulepart, original_file, doctemplate, langcode, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { modulepart, original_file };
if (doctemplate) body.doctemplate = doctemplate;
if (langcode) body.langcode = langcode;
return formatResponse(await client.put('/documents/builddoc', body));
},
);
// ── Members (Associations) ──────────────────────────────────────────────
server.tool(
'dolibarr_members_list',
'List association members',
{
typeid: z.number().optional().describe('Filter by member type ID'),
status: z.enum(['0', '1', '-1', '-2']).optional().describe('1=validated, 0=draft, -1=resigned, -2=excluded'),
search: z.string().optional().describe('Search in name'),
...PaginationParams,
...ConnectionParam,
},
async ({ typeid, status, search, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (typeid !== undefined) params['typeid'] = String(typeid);
if (status) params['status'] = status;
if (search) params['sqlfilters'] = searchFilter('t.lastname', search);
return formatResponse(await client.get('/members', params));
},
);
server.tool(
'dolibarr_member_get',
'Get a single member by ID',
{
id: z.number().describe('Member ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/members/${id}`));
},
);
// ── Stock Movements ─────────────────────────────────────────────────────
server.tool(
'dolibarr_stockmovements_list',
'List stock movements',
{
product_id: z.number().optional().describe('Filter by product ID'),
warehouse_id: z.number().optional().describe('Filter by warehouse ID'),
...PaginationParams,
...ConnectionParam,
},
async ({ product_id, warehouse_id, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params: Record<string, string> = { ...paginationQuery({ limit, page, sortfield, sortorder }) };
if (product_id !== undefined) params['productidselected'] = String(product_id);
if (warehouse_id !== undefined) params['warehouseidselected'] = String(warehouse_id);
return formatResponse(await client.get('/stockmovements', params));
},
);
server.tool(
'dolibarr_stockmovement_create',
'Create a stock movement (add/remove stock)',
{
product_id: z.number().describe('Product ID'),
warehouse_id: z.number().describe('Warehouse ID'),
qty: z.number().describe('Quantity (positive=add, negative=remove)'),
type: z.enum(['0', '1', '2', '3']).optional().describe('0=increase, 1=decrease, 2=transfer increase, 3=transfer decrease'),
label: z.string().optional().describe('Movement label/reason'),
inventorycode: z.string().optional().describe('Inventory code'),
...ConnectionParam,
},
async ({ product_id, warehouse_id, qty, type, label, inventorycode, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { product_id, warehouse_id, qty };
if (type !== undefined) body.type = type;
if (label) body.label = label;
if (inventorycode) body.inventorycode = inventorycode;
return formatResponse(await client.post('/stockmovements', body));
},
);
// ── Contact CRUD ────────────────────────────────────────────────────────
server.tool(
'dolibarr_contact_create',
'Create a new contact/address',
{
lastname: z.string().describe('Last name'),
firstname: z.string().optional().describe('First name'),
socid: z.number().optional().describe('Third party ID'),
poste: z.string().optional().describe('Job title/position'),
email: z.string().optional().describe('Email address'),
phone_pro: z.string().optional().describe('Professional phone'),
phone_mobile: z.string().optional().describe('Mobile phone'),
address: z.string().optional().describe('Street address'),
zip: z.string().optional().describe('Postal code'),
town: z.string().optional().describe('City'),
country_id: z.number().optional().describe('Country ID'),
...ConnectionParam,
},
async ({ lastname, firstname, socid, poste, email, phone_pro, phone_mobile, address, zip, town, country_id, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { lastname };
if (firstname) body.firstname = firstname;
if (socid !== undefined) body.socid = socid;
if (poste) body.poste = poste;
if (email) body.email = email;
if (phone_pro) body.phone_pro = phone_pro;
if (phone_mobile) body.phone_mobile = phone_mobile;
if (address) body.address = address;
if (zip) body.zip = zip;
if (town) body.town = town;
if (country_id !== undefined) body.country_id = country_id;
return formatResponse(await client.post('/contacts', body));
},
);
server.tool(
'dolibarr_contact_update',
'Update a contact/address',
{
id: z.number().describe('Contact ID'),
lastname: z.string().optional().describe('Last name'),
firstname: z.string().optional().describe('First name'),
email: z.string().optional().describe('Email address'),
phone_pro: z.string().optional().describe('Professional phone'),
phone_mobile: z.string().optional().describe('Mobile phone'),
poste: z.string().optional().describe('Job title/position'),
...ConnectionParam,
},
async ({ id, lastname, firstname, email, phone_pro, phone_mobile, poste, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = {};
if (lastname !== undefined) body.lastname = lastname;
if (firstname !== undefined) body.firstname = firstname;
if (email !== undefined) body.email = email;
if (phone_pro !== undefined) body.phone_pro = phone_pro;
if (phone_mobile !== undefined) body.phone_mobile = phone_mobile;
if (poste !== undefined) body.poste = poste;
return formatResponse(await client.put(`/contacts/${id}`, body));
},
);
server.tool(
'dolibarr_contact_delete',
'Delete a contact/address',
{
id: z.number().describe('Contact ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.delete(`/contacts/${id}`));
},
);
// ── Order Lines ─────────────────────────────────────────────────────────
server.tool(
'dolibarr_order_add_line',
'Add a line to a customer order',
{
id: z.number().describe('Order ID'),
desc: z.string().describe('Line description'),
subprice: z.number().describe('Unit price (HT)'),
qty: z.number().describe('Quantity'),
tva_tx: z.number().optional().describe('VAT rate (e.g. 20.0)'),
product_id: z.number().optional().describe('Product/service ID'),
...ConnectionParam,
},
async ({ id, desc, subprice, qty, tva_tx, product_id, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { desc, subprice, qty };
if (tva_tx !== undefined) body.tva_tx = tva_tx;
if (product_id !== undefined) body.fk_product = product_id;
return formatResponse(await client.post(`/orders/${id}/lines`, body));
},
);
// ── Project Tasks CRUD ──────────────────────────────────────────────────
server.tool(
'dolibarr_task_create',
'Create a new project task',
{
fk_project: z.number().describe('Project ID'),
label: z.string().describe('Task label'),
description: z.string().optional().describe('Task description'),
date_start: z.string().optional().describe('Start date (YYYY-MM-DD or Unix timestamp)'),
date_end: z.string().optional().describe('End date'),
planned_workload: z.number().optional().describe('Planned workload in seconds'),
progress: z.number().optional().describe('Progress percentage (0-100)'),
...ConnectionParam,
},
async ({ fk_project, label, description, date_start, date_end, planned_workload, progress, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { fk_project, label };
if (description) body.description = description;
if (date_start) body.date_start = date_start;
if (date_end) body.date_end = date_end;
if (planned_workload !== undefined) body.planned_workload = planned_workload;
if (progress !== undefined) body.progress = progress;
return formatResponse(await client.post('/tasks', body));
},
);
server.tool(
'dolibarr_task_update',
'Update a project task',
{
id: z.number().describe('Task ID'),
label: z.string().optional().describe('Task label'),
description: z.string().optional().describe('Task description'),
date_start: z.string().optional().describe('Start date'),
date_end: z.string().optional().describe('End date'),
progress: z.number().optional().describe('Progress percentage (0-100)'),
...ConnectionParam,
},
async ({ id, label, description, date_start, date_end, progress, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = {};
if (label !== undefined) body.label = label;
if (description !== undefined) body.description = description;
if (date_start !== undefined) body.date_start = date_start;
if (date_end !== undefined) body.date_end = date_end;
if (progress !== undefined) body.progress = progress;
return formatResponse(await client.put(`/tasks/${id}`, body));
},
);
server.tool(
'dolibarr_task_timespent_list',
'List time spent entries for a task',
{
id: z.number().describe('Task ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/tasks/${id}/timespent`));
},
);
server.tool(
'dolibarr_task_timespent_add',
'Add time spent on a task',
{
id: z.number().describe('Task ID'),
date: z.string().describe('Date of work (YYYY-MM-DD or Unix timestamp)'),
duration: z.number().describe('Duration in seconds'),
fk_user: z.number().describe('User ID who did the work'),
note: z.string().optional().describe('Note about the work done'),
...ConnectionParam,
},
async ({ id, date, duration, fk_user, note, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { date, duration, fk_user };
if (note) body.note = note;
return formatResponse(await client.post(`/tasks/${id}/timespent`, body));
},
);
// ── Project Update ──────────────────────────────────────────────────────
server.tool(
'dolibarr_project_update',
'Update a project',
{
id: z.number().describe('Project ID'),
title: z.string().optional().describe('Project title'),
description: z.string().optional().describe('Description'),
date_start: z.string().optional().describe('Start date'),
date_end: z.string().optional().describe('End date'),
status: z.enum(['0', '1', '2']).optional().describe('0=draft, 1=open, 2=closed'),
...ConnectionParam,
},
async ({ id, title, description, date_start, date_end, status, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = {};
if (title !== undefined) body.title = title;
if (description !== undefined) body.description = description;
if (date_start !== undefined) body.date_start = date_start;
if (date_end !== undefined) body.date_end = date_end;
if (status !== undefined) body.status = status;
return formatResponse(await client.put(`/projects/${id}`, body));
},
);
// ── Dictionary / Setup ──────────────────────────────────────────────────
server.tool(
'dolibarr_setup_dictionary',
'Get dictionary entries (countries, currencies, payment types, etc.)',
{
type: z.enum([
'countries', 'regions', 'states', 'currencies', 'civilities',
'payment_types', 'shipping_methods', 'availability',
'order_methods', 'event_types', 'expense_report_types',
'ticket_types', 'ticket_severities', 'ticket_categories',
'units', 'legal_form', 'staff_range', 'typent',
]).describe('Dictionary type to retrieve'),
...PaginationParams,
...ConnectionParam,
},
async ({ type, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params = paginationQuery({ limit, page, sortfield, sortorder });
return formatResponse(await client.get(`/setup/dictionary/${type}`, params));
},
);
server.tool(
'dolibarr_setup_modules',
'List enabled Dolibarr modules',
{ ...ConnectionParam },
async ({ connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get('/setup/modules'));
},
);
// ── Bank Account Transactions ───────────────────────────────────────────
server.tool(
'dolibarr_bankaccount_lines',
'List transactions/lines for a bank account',
{
id: z.number().describe('Bank account ID'),
...PaginationParams,
...ConnectionParam,
},
async ({ id, limit, page, sortfield, sortorder, connection }) => {
const client = clientFor(connection);
const params = paginationQuery({ limit, page, sortfield, sortorder });
return formatResponse(await client.get(`/bankaccounts/${id}/lines`, params));
},
);
// ── Category CRUD ───────────────────────────────────────────────────────
server.tool(
'dolibarr_category_get',
'Get a single category by ID',
{
id: z.number().describe('Category ID'),
...ConnectionParam,
},
async ({ id, connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get(`/categories/${id}`));
},
);
server.tool(
'dolibarr_category_create',
'Create a new category',
{
label: z.string().describe('Category label'),
type: z.enum(['product', 'supplier', 'customer', 'member', 'contact', 'project']).describe('Category type'),
fk_parent: z.number().optional().describe('Parent category ID (0 = root)'),
description: z.string().optional().describe('Category description'),
color: z.string().optional().describe('Category color (hex, e.g. "ff0000")'),
...ConnectionParam,
},
async ({ label, type, fk_parent, description, color, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { label, type };
if (fk_parent !== undefined) body.fk_parent = fk_parent;
if (description) body.description = description;
if (color) body.color = color;
return formatResponse(await client.post('/categories', body));
},
);
// ── User CRUD ───────────────────────────────────────────────────────────
server.tool(
'dolibarr_user_create',
'Create a new Dolibarr user',
{
login: z.string().describe('Login/username'),
lastname: z.string().describe('Last name'),
firstname: z.string().optional().describe('First name'),
email: z.string().optional().describe('Email address'),
admin: z.enum(['0', '1']).optional().describe('0=user, 1=admin'),
employee: z.enum(['0', '1']).optional().describe('1=is employee'),
...ConnectionParam,
},
async ({ login, lastname, firstname, email, admin, employee, connection }) => {
const client = clientFor(connection);
const body: Record<string, unknown> = { login, lastname };
if (firstname) body.firstname = firstname;
if (email) body.email = email;
if (admin) body.admin = admin;
if (employee) body.employee = employee;
return formatResponse(await client.post('/users', body));
},
);
// ── Setup / Dictionary ──────────────────────────────────────────────────
server.tool(
'dolibarr_setup_company',
'Get company/entity information',
{ ...ConnectionParam },
async ({ connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get('/setup/company'));
},
);
server.tool(
'dolibarr_status',
'Get Dolibarr instance status and version',
{ ...ConnectionParam },
async ({ connection }) => {
const client = clientFor(connection);
return formatResponse(await client.get('/status'));
},
);
// ── Generic API Call ────────────────────────────────────────────────────
server.tool(
'dolibarr_api_request',
'Make a raw API request to any Dolibarr REST endpoint',
{
method: z.enum(['GET', 'POST', 'PUT', 'DELETE']).describe('HTTP method'),
endpoint: z.string().describe('API endpoint path (e.g. "/thirdparties")'),
body: z.record(z.string(), z.unknown()).optional().describe('Request body for POST/PUT'),
params: z.record(z.string(), z.string()).optional().describe('Query parameters'),
...ConnectionParam,
},
async ({ method, endpoint, body, params, connection }) => {
const client = clientFor(connection);
switch (method) {
case 'GET':
return formatResponse(await client.get(endpoint, params));
case 'POST':
return formatResponse(await client.post(endpoint, body));
case 'PUT':
return formatResponse(await client.put(endpoint, body));
case 'DELETE':
return formatResponse(await client.delete(endpoint));
}
},
);
// ── Connections Management ──────────────────────────────────────────────
server.tool(
'dolibarr_list_connections',
'List configured Dolibarr API connections',
{},
async () => {
const lines = Object.entries(config.connections).map(([name, conn]) => {
const is_default = name === config.defaultConnection ? ' (default)' : '';
return ` ${name}${is_default}: ${conn.baseUrl}`;
});
return {
content: [{ type: 'text' as const, text: `Configured connections:\n${lines.join('\n')}` }],
};
},
);
// ── Start Server ────────────────────────────────────────────────────────
async function main(): Promise<void> {
config = await loadConfig();
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
process.stderr.write(`Fatal: ${err}\n`);
process.exit(1);
});