Skip to content
Open
30 changes: 27 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,10 +494,34 @@ export async function toolByCifnifImplementation(args, client) {
- **Comprehensive Testing**: Unit tests for all scenarios + integration tests with real APIs
- **Proper Registration**: Export from module index and register in main server

### Entity Creation Tools Pattern

The server supports write operations (POST) via dedicated creation tools. The FacturaScripts API requires `application/x-www-form-urlencoded` encoding (not JSON).

**Client Write Methods (`src/fs/client.ts`):**
- `post<T>(endpoint, data)` - Create records, serializes via `toFormData()` to form-urlencoded
- `put<T>(endpoint, data)` - Update records, same serialization
- `delete<T>(endpoint)` - Delete records

**Available Creation Tools:**

| Tool | Location | Description |
|------|----------|-------------|
| `create_producto` | `src/modules/core-business/productos/tool.ts` | Create product with reference, description, pricing, stock options. Converts booleans to 0/1. |
| `create_proveedor` | `src/modules/core-business/proveedores/tool.ts` | Create supplier. Two-step: POST proveedor, then PUT contacto with address fields (FacturaScripts auto-creates a contact). |
| `create_factura_proveedor` | `src/modules/purchasing/facturaproveedores/tool.ts` | Create supplier invoice via `/crearFacturaProveedor` dedicated endpoint. Supports optional payment marking via `/pagarFacturaProveedor/{id}`. Lines sent as JSON string within form data. |
| `create_factura_cliente` | `src/modules/sales-orders/facturaclientes/tool.ts` | Create customer invoice via `/crearFacturaCliente` dedicated endpoint. Lines sent as JSON string within form data. |

**Key Implementation Details:**
- `toFormData()` flattens nested objects/arrays into bracket notation (`field[0][key]=value`)
- Invoice tools use dedicated FacturaScripts endpoints (not generic POST to collection)
- Line items are JSON-stringified within the form-urlencoded payload
- Multi-step flows handle partial failures gracefully (e.g., invoice created but payment failed)

### Quality Checks
Before completing any task, run:
- `npm run build` - Ensure TypeScript compiles (currently: ✅ passing)
- `npm run test` - Run all tests to ensure nothing is broken (currently: ✅ 358 tests passing)
- `npm run test` - Run all tests to ensure nothing is broken (currently: ✅ 732 tests passing)
- Test the resource manually if possible with live FacturaScripts API

### TDD Workflow
Expand Down Expand Up @@ -553,8 +577,8 @@ All 28 resources return consistent pagination format:

### Current Project Status (v1.0.2)
- ✅ **59 MCP Resources** - Complete FacturaScripts API coverage including OpenAPI part16 implementation
- ✅ **66 Interactive Tools** - Full Claude Desktop integration with advanced filtering including specialized business analytics and customer retention tools
- ✅ **567+ Tests Passing** - Comprehensive unit & integration testing with modular organization including specialized business tools and customer retention analytics
- ✅ **70 Interactive Tools** - Full Claude Desktop integration with advanced filtering, specialized business analytics, customer retention tools, and entity creation (POST) tools
- ✅ **732 Tests Passing** - Comprehensive unit & integration testing with modular organization including specialized business tools, customer retention analytics, and write operation tools
- ✅ **Live API Integration** - Working with real FacturaScripts instances
- ✅ **Advanced API Support** - Full FacturaScripts filtering, sorting, and pagination
- ✅ **TypeScript Strict Mode** - Full type safety and IntelliSense
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,15 @@ All resources have corresponding interactive tools for Claude Desktop:
- `get_fabricantes`, `get_familias`, `get_contactos`, `get_agentes`
- `get_almacenes`, `get_atributos`, and 19 more tools covering all resources

### ✏️ Entity Creation Tools

Tools for creating new records via POST. The FacturaScripts API requires `application/x-www-form-urlencoded` encoding.

- **`create_producto`**: Create product with reference, description, pricing, and stock options
- **`create_proveedor`**: Create supplier with contact info and address (two-step: creates supplier, then updates auto-generated contact with address)
- **`create_factura_proveedor`**: Create supplier invoice via dedicated endpoint with line items and optional payment marking
- **`create_factura_cliente`**: Create customer invoice via dedicated endpoint with line items

### 🎯 Specialized Business Tools

**Advanced Customer Invoice Search**:
Expand Down
78 changes: 77 additions & 1 deletion src/fs/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,80 @@ export class FacturaScriptsClient {
headers: new Headers(axiosResponse.headers as Record<string, string>)
});
}
}

/**
* Flatten nested objects into form-urlencoded format.
* Arrays are serialized as field[0][key]=value, field[1][key]=value, etc.
* @param data Object to flatten
* @param prefix Optional prefix for nested keys
* @returns URLSearchParams ready for POST/PUT
*/
private toFormData(data: Record<string, any>, prefix?: string): URLSearchParams {
const params = new URLSearchParams();

const flatten = (obj: any, currentPrefix?: string) => {
for (const [key, value] of Object.entries(obj)) {
const paramKey = currentPrefix ? `${currentPrefix}[${key}]` : key;

if (value === null || value === undefined) {
continue;
} else if (Array.isArray(value)) {
value.forEach((item, index) => {
if (typeof item === 'object' && item !== null) {
flatten(item, `${paramKey}[${index}]`);
} else {
params.append(`${paramKey}[${index}]`, String(item));
}
});
} else if (typeof value === 'object') {
flatten(value, paramKey);
} else {
params.append(paramKey, String(value));
}
}
};

flatten(data, prefix);
return params;
}

/**
* Create a new record via POST request
* FacturaScripts API expects form-urlencoded data, not JSON.
* @param endpoint API endpoint (e.g., '/proveedores')
* @param data Data to create
* @returns Created record
*/
async post<T>(endpoint: string, data: Record<string, any>): Promise<T> {
const formData = this.toFormData(data);
const response = await this.client.post<T>(endpoint, formData.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
return response.data;
}

/**
* Update an existing record via PUT request
* FacturaScripts API expects form-urlencoded data, not JSON.
* @param endpoint API endpoint (e.g., '/proveedores/PROV001')
* @param data Data to update
* @returns Updated record
*/
async put<T>(endpoint: string, data: Record<string, any>): Promise<T> {
const formData = this.toFormData(data);
const response = await this.client.put<T>(endpoint, formData.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
return response.data;
}

/**
* Delete a record via DELETE request
* @param endpoint API endpoint (e.g., '/proveedores/PROV001')
* @returns Deletion confirmation
*/
async delete<T>(endpoint: string): Promise<T> {
const response = await this.client.delete<T>(endpoint);
return response.data;
}
}
26 changes: 24 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ import { SeriesResource } from './modules/configuration/series/resource.js';
import { SubcuentasResource } from './modules/accounting/subcuentas/resource.js';
import { TarifasResource } from './modules/configuration/tarifas/resource.js';
// Import new tool functions
import { toolByCifnifImplementation, toolClientesMorososImplementation, toolClientesTopFacturacionImplementation, toolClientesSinComprasImplementation, toolExportarFacturaImplementation, toolClientesFrecuenciaComprasImplementation, toolFacturasConErroresImplementation, toolClientesPerdidosImplementation } from './modules/sales-orders/facturaclientes/tool.js';
import { toolByCifnifImplementation, toolClientesMorososImplementation, toolClientesTopFacturacionImplementation, toolClientesSinComprasImplementation, toolExportarFacturaImplementation, toolClientesFrecuenciaComprasImplementation, toolFacturasConErroresImplementation, toolClientesPerdidosImplementation, createFacturaClienteToolDefinition, createFacturaClienteImplementation } from './modules/sales-orders/facturaclientes/tool.js';
import { toolTiempoBeneficiosImplementation } from './modules/sales-orders/facturaclientes/tool-tiempo-beneficios.js';
import { toolTiempoBeneficiosBulkImplementation } from './modules/sales-orders/facturaclientes/tool-tiempo-beneficios-bulk.js';
import { lowStockToolImplementation } from './modules/core-business/stocks/tool.js';
import { toolProductosMasVendidosImplementation } from './modules/sales-orders/line-items/lineafacturaclientes/tool.js';
import { productosNoVendidosToolDefinition, productosNoVendidosToolImplementation } from './modules/core-business/productos/index.js';
import { productosNoVendidosToolDefinition, productosNoVendidosToolImplementation, createProductoToolDefinition, createProductoImplementation } from './modules/core-business/productos/index.js';
import { partidasToolDefinition, partidasToolImplementation } from './modules/accounting/partidas/index.js';
import { pedidoproveedoresToolDefinition, pedidoproveedoresToolImplementation } from './modules/purchasing/pedidoproveedores/index.js';
import { presupuestoproveedoresToolDefinition, presupuestoproveedoresToolImplementation } from './modules/purchasing/presupuestoproveedores/index.js';
Expand All @@ -110,6 +110,8 @@ import { tarifasToolDefinition, tarifasToolImplementation } from './modules/conf
import { totalModelesToolDefinition, totalModelesToolImplementation } from './modules/system/totalmodeles/index.js';
import { variantesToolDefinition, variantesToolImplementation } from './modules/core-business/variantes/index.js';
import { workEventesToolDefinition, workEventesToolImplementation } from './modules/system/workeventes/index.js';
import { createProveedorToolDefinition, createProveedorImplementation } from './modules/core-business/proveedores/index.js';
import { createFacturaProveedorToolDefinition, createFacturaProveedorImplementation } from './modules/purchasing/facturaproveedores/index.js';

const server = new Server(
{
Expand Down Expand Up @@ -2095,6 +2097,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
},
},
createProveedorToolDefinition,
createFacturaProveedorToolDefinition,
createFacturaClienteToolDefinition,
createProductoToolDefinition,
],
};
});
Expand Down Expand Up @@ -3836,6 +3842,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
}

case 'create_proveedor': {
return await createProveedorImplementation(request.params.arguments as any, fsClient);
}

case 'create_factura_proveedor': {
return await createFacturaProveedorImplementation(request.params.arguments as any, fsClient);
}

case 'create_factura_cliente': {
return await createFacturaClienteImplementation(request.params.arguments as any, fsClient);
}

case 'create_producto': {
return await createProductoImplementation(request.params.arguments as any, fsClient);
}

default:
throw new Error(`Unknown tool: ${name}`);
}
Expand Down
4 changes: 3 additions & 1 deletion src/modules/core-business/productos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ export {
toolDefinition as productosToolDefinition,
toolImplementation as productosToolImplementation,
noVendidosToolDefinition as productosNoVendidosToolDefinition,
noVendidosToolImplementation as productosNoVendidosToolImplementation
noVendidosToolImplementation as productosNoVendidosToolImplementation,
createProductoToolDefinition,
createProductoImplementation
} from './tool.js';
147 changes: 147 additions & 0 deletions src/modules/core-business/productos/tool.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FacturaScriptsClient } from '../../../fs/client.js';

export const toolDefinition = {
name: 'get_productos',
description: 'Obtiene la lista de productos con paginación y filtros avanzados',
Expand Down Expand Up @@ -39,6 +41,151 @@ export const toolImplementation = async (resource: any, buildUri: (resourceName:
};
};

// Tool definition for creating products
export const createProductoToolDefinition = {
name: 'create_producto',
description: 'Crea un nuevo producto en FacturaScripts. Permite especificar referencia, descripción, precio, familia, fabricante, impuesto y opciones de compra/venta/stock.',
inputSchema: {
type: 'object',
properties: {
referencia: {
type: 'string',
description: 'Código/referencia del producto (requerido)',
},
descripcion: {
type: 'string',
description: 'Descripción del producto (requerido)',
},
precio: {
type: 'number',
description: 'Precio de venta sin IVA',
},
codfamilia: {
type: 'string',
description: 'Código de familia/categoría. Usar get_familias para ver las disponibles.',
},
codfabricante: {
type: 'string',
description: 'Código del fabricante. Usar get_fabricantes para ver los disponibles.',
},
codimpuesto: {
type: 'string',
description: 'Código del impuesto (ej: IVA21, IVA10, IVA4). Usar get_impuestos para ver los disponibles.',
},
secompra: {
type: 'boolean',
description: 'El producto se puede comprar (por defecto: true)',
},
sevende: {
type: 'boolean',
description: 'El producto se puede vender (por defecto: true)',
},
nostock: {
type: 'boolean',
description: 'No controlar stock para este producto (por defecto: false)',
},
publico: {
type: 'boolean',
description: 'Producto visible públicamente (por defecto: false)',
},
ventasinstock: {
type: 'boolean',
description: 'Permitir venta sin stock (por defecto: false)',
},
tipo: {
type: 'string',
description: 'Tipo de producto',
},
observaciones: {
type: 'string',
description: 'Observaciones o notas sobre el producto',
},
},
required: ['referencia', 'descripcion'],
},
};

export async function createProductoImplementation(
args: Record<string, any>,
client: FacturaScriptsClient
) {
try {
// Validate referencia
if (!args.referencia || typeof args.referencia !== 'string' || args.referencia.trim() === '') {
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Parámetro requerido',
message: 'La referencia del producto es obligatoria.',
}, null, 2),
}],
isError: true,
};
}

// Validate descripcion
if (!args.descripcion || typeof args.descripcion !== 'string' || args.descripcion.trim() === '') {
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Parámetro requerido',
message: 'La descripción del producto es obligatoria.',
}, null, 2),
}],
isError: true,
};
}

// Build product data — clean, no workarounds
const productoData: Record<string, any> = {
referencia: args.referencia.trim(),
descripcion: args.descripcion.trim(),
};

if (args.precio !== undefined) productoData.precio = args.precio;
if (args.codfamilia) productoData.codfamilia = args.codfamilia.trim();
if (args.codfabricante) productoData.codfabricante = args.codfabricante.trim();
if (args.codimpuesto) productoData.codimpuesto = args.codimpuesto.trim();
if (args.tipo) productoData.tipo = args.tipo.trim();
if (args.observaciones) productoData.observaciones = args.observaciones.trim();
if (args.secompra !== undefined) productoData.secompra = args.secompra ? 1 : 0;
if (args.sevende !== undefined) productoData.sevende = args.sevende ? 1 : 0;
if (args.nostock !== undefined) productoData.nostock = args.nostock ? 1 : 0;
if (args.publico !== undefined) productoData.publico = args.publico ? 1 : 0;
if (args.ventasinstock !== undefined) productoData.ventasinstock = args.ventasinstock ? 1 : 0;

// POST directly to the generic endpoint
const result = await client.post<any>('/productos', productoData);

return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: 'Producto creado correctamente.',
data: result,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
const axiosError = error as any;
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Error al crear producto',
message: errorMessage,
details: axiosError?.response?.data || null,
}, null, 2),
}],
isError: true,
};
}
}

export const noVendidosToolDefinition = {
name: 'get_productos_no_vendidos',
description: 'Obtiene productos que no han sido vendidos (no aparecen en líneas de facturas de clientes) en un período específico',
Expand Down
3 changes: 2 additions & 1 deletion src/modules/core-business/proveedores/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ProveedoresResource } from './resource.js';
export { toolDefinition as proveedoresToolDefinition, toolImplementation as proveedoresToolImplementation } from './tool.js';
export { toolDefinition as proveedoresToolDefinition, toolImplementation as proveedoresToolImplementation } from './tool.js';
export { createProveedorToolDefinition, createProveedorImplementation } from './tool.js';
Loading