#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { OdooConnection, OdooConfig } from './odoo-connection.js';
import { loadEnvConfig, hasCompleteConfig } from './config.js';
// Connection instance (lazy initialization)
let odooConnection: OdooConnection | null = null;
// Load environment config
const envConfig = loadEnvConfig();
// Server setup
const server = new Server(
{
name: 'mcp-odoo-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Helper function to get or create connection
function getConnection(config?: OdooConfig): OdooConnection {
if (!odooConnection && config) {
odooConnection = new OdooConnection(config);
}
if (!odooConnection) {
throw new Error('Odoo connection not initialized. Please call connect first.');
}
return odooConnection;
}
// Define tools
const tools: Tool[] = [
{
name: 'odoo_connect_env',
description: 'Connect to Odoo using credentials from .env file. The .env file should contain ODOO_HOST, ODOO_DATABASE, ODOO_USERNAME, ODOO_API_KEY, and optionally ODOO_PORT and ODOO_PROTOCOL.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'odoo_connect',
description: 'Connect to an Odoo database using XML-RPC with manually provided credentials. Must be called before other operations.',
inputSchema: {
type: 'object',
properties: {
host: {
type: 'string',
description: 'Odoo server hostname (e.g., localhost or example.odoo.com)',
},
database: {
type: 'string',
description: 'Database name',
},
username: {
type: 'string',
description: 'User email',
},
apiKey: {
type: 'string',
description: 'API key generated from Odoo user preferences',
},
port: {
type: 'number',
description: 'Port number (default: 8069)',
},
protocol: {
type: 'string',
enum: ['http', 'https'],
description: 'Protocol to use (default: http)',
},
},
required: ['host', 'database', 'username', 'apiKey'],
},
},
{
name: 'odoo_search',
description: 'Search for records in an Odoo model. Returns record IDs matching the domain.',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name (e.g., res.partner, sale.order)',
},
domain: {
type: 'array',
description: 'Search domain in Odoo format (e.g., [["name", "ilike", "John"]])',
default: [],
},
offset: {
type: 'number',
description: 'Number of records to skip',
},
limit: {
type: 'number',
description: 'Maximum number of records to return',
},
order: {
type: 'string',
description: 'Order by clause (e.g., "name ASC, id DESC")',
},
},
required: ['model'],
},
},
{
name: 'odoo_search_read',
description: 'Search and read records from an Odoo model in a single call.',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name (e.g., res.partner, sale.order)',
},
domain: {
type: 'array',
description: 'Search domain in Odoo format',
default: [],
},
fields: {
type: 'array',
items: { type: 'string' },
description: 'List of field names to retrieve (empty array returns all fields)',
default: [],
},
offset: {
type: 'number',
description: 'Number of records to skip',
},
limit: {
type: 'number',
description: 'Maximum number of records to return',
},
order: {
type: 'string',
description: 'Order by clause',
},
},
required: ['model'],
},
},
{
name: 'odoo_read',
description: 'Read specific records by their IDs.',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name',
},
ids: {
type: 'array',
items: { type: 'number' },
description: 'List of record IDs to read',
},
fields: {
type: 'array',
items: { type: 'string' },
description: 'List of field names to retrieve (empty array returns all fields)',
default: [],
},
},
required: ['model', 'ids'],
},
},
{
name: 'odoo_create',
description: 'Create a new record in an Odoo model.',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name',
},
values: {
type: 'object',
description: 'Field values for the new record',
},
},
required: ['model', 'values'],
},
},
{
name: 'odoo_write',
description: 'Update existing records in an Odoo model.',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name',
},
ids: {
type: 'array',
items: { type: 'number' },
description: 'List of record IDs to update',
},
values: {
type: 'object',
description: 'Field values to update',
},
},
required: ['model', 'ids', 'values'],
},
},
{
name: 'odoo_delete',
description: 'Delete records from an Odoo model.',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name',
},
ids: {
type: 'array',
items: { type: 'number' },
description: 'List of record IDs to delete',
},
},
required: ['model', 'ids'],
},
},
{
name: 'odoo_fields_get',
description: 'Get field definitions for an Odoo model.',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name',
},
fields: {
type: 'array',
items: { type: 'string' },
description: 'Specific fields to get info about (empty for all fields)',
default: [],
},
attributes: {
type: 'array',
items: { type: 'string' },
description: 'Specific attributes to retrieve (e.g., string, type, required)',
default: [],
},
},
required: ['model'],
},
},
{
name: 'odoo_execute',
description: 'Execute any method on an Odoo model (advanced usage).',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name',
},
method: {
type: 'string',
description: 'Method name to execute',
},
args: {
type: 'array',
description: 'Positional arguments for the method',
default: [],
},
kwargs: {
type: 'object',
description: 'Keyword arguments for the method',
default: {},
},
},
required: ['model', 'method'],
},
},
{
name: 'odoo_version',
description: 'Get Odoo server version information.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'odoo_check_access_rights',
description: 'Check if the current user has access rights for a specific operation on a model.',
inputSchema: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Odoo model name',
},
operation: {
type: 'string',
enum: ['read', 'write', 'create', 'unlink'],
description: 'Operation to check access for',
},
raiseException: {
type: 'boolean',
description: 'Whether to raise an exception if access is denied',
default: false,
},
},
required: ['model', 'operation'],
},
},
{
name: 'odoo_get_installed_modules',
description: 'Get list of installed Odoo modules.',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of modules to return',
},
},
},
},
{
name: 'odoo_install_module',
description: 'Install an Odoo module by its technical name.',
inputSchema: {
type: 'object',
properties: {
module_name: {
type: 'string',
description: 'Technical name of the module to install',
},
},
required: ['module_name'],
},
},
{
name: 'odoo_uninstall_module',
description: 'Uninstall an Odoo module by its technical name.',
inputSchema: {
type: 'object',
properties: {
module_name: {
type: 'string',
description: 'Technical name of the module to uninstall',
},
},
required: ['module_name'],
},
},
{
name: 'odoo_upgrade_module',
description: 'Upgrade an Odoo module to the latest version.',
inputSchema: {
type: 'object',
properties: {
module_name: {
type: 'string',
description: 'Technical name of the module to upgrade',
},
},
required: ['module_name'],
},
},
];
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (!args) {
throw new Error('No arguments provided');
}
switch (name) {
case 'odoo_connect_env': {
if (!hasCompleteConfig(envConfig)) {
throw new Error(
'Incomplete .env configuration. Please ensure .env file exists with: ODOO_HOST, ODOO_DATABASE, ODOO_USERNAME, and ODOO_API_KEY'
);
}
const config: OdooConfig = {
host: envConfig.host!,
database: envConfig.database!,
username: envConfig.username!,
apiKey: envConfig.apiKey!,
port: envConfig.port || 8069,
protocol: envConfig.protocol || 'http',
};
odooConnection = new OdooConnection(config);
await odooConnection.authenticate();
return {
content: [
{
type: 'text',
text: `Successfully connected to Odoo database '${config.database}' at ${config.host}:${config.port} using .env configuration`,
},
],
};
}
case 'odoo_connect': {
const config: OdooConfig = {
host: args.host as string,
database: args.database as string,
username: args.username as string,
apiKey: args.apiKey as string,
port: args.port as number | undefined,
protocol: args.protocol as 'http' | 'https' | undefined,
};
odooConnection = new OdooConnection(config);
await odooConnection.authenticate();
return {
content: [
{
type: 'text',
text: `Successfully connected to Odoo database '${config.database}' at ${config.host}`,
},
],
};
}
case 'odoo_search': {
const connection = getConnection();
const result = await connection.search(
args.model as string,
args.domain as any[] || [],
{
offset: args.offset as number | undefined,
limit: args.limit as number | undefined,
order: args.order as string | undefined,
}
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'odoo_search_read': {
const connection = getConnection();
const result = await connection.searchRead(
args.model as string,
args.domain as any[] || [],
args.fields as string[] || [],
{
offset: args.offset as number | undefined,
limit: args.limit as number | undefined,
order: args.order as string | undefined,
}
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'odoo_read': {
const connection = getConnection();
const result = await connection.read(
args.model as string,
args.ids as number[],
args.fields as string[] || []
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'odoo_create': {
const connection = getConnection();
const result = await connection.create(
args.model as string,
args.values as Record<string, any>
);
return {
content: [
{
type: 'text',
text: `Created record with ID: ${result}`,
},
],
};
}
case 'odoo_write': {
const connection = getConnection();
const ids = args.ids as number[];
const result = await connection.write(
args.model as string,
ids,
args.values as Record<string, any>
);
return {
content: [
{
type: 'text',
text: `Updated ${ids.length} record(s): ${result}`,
},
],
};
}
case 'odoo_delete': {
const connection = getConnection();
const ids = args.ids as number[];
const result = await connection.unlink(
args.model as string,
ids
);
return {
content: [
{
type: 'text',
text: `Deleted ${ids.length} record(s): ${result}`,
},
],
};
}
case 'odoo_fields_get': {
const connection = getConnection();
const result = await connection.fieldsGet(
args.model as string,
args.fields as string[] || [],
args.attributes as string[] || []
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'odoo_execute': {
const connection = getConnection();
const result = await connection.execute(
args.model as string,
args.method as string,
args.args as any[] || [],
args.kwargs as Record<string, any> || {}
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'odoo_version': {
const connection = getConnection();
const result = await connection.version();
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'odoo_check_access_rights': {
const connection = getConnection();
const result = await connection.checkAccessRights(
args.model as string,
args.operation as 'read' | 'write' | 'create' | 'unlink',
args.raiseException as boolean || false
);
return {
content: [
{
type: 'text',
text: `Access ${result ? 'granted' : 'denied'} for ${args.operation} on ${args.model}`,
},
],
};
}
case 'odoo_get_installed_modules': {
const connection = getConnection();
const result = await connection.searchRead(
'ir.module.module',
[['state', '=', 'installed']],
['name', 'shortdesc', 'installed_version', 'author'],
{ limit: args.limit as number | undefined }
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'odoo_install_module': {
const connection = getConnection();
// Find the module
const moduleIds = await connection.search('ir.module.module', [
['name', '=', args.module_name],
]);
if (moduleIds.length === 0) {
throw new Error(`Module '${args.module_name}' not found`);
}
// Install the module
await connection.execute('ir.module.module', 'button_immediate_install', [
moduleIds,
]);
return {
content: [
{
type: 'text',
text: `Module '${args.module_name}' installed successfully`,
},
],
};
}
case 'odoo_uninstall_module': {
const connection = getConnection();
// Find the module
const moduleIds = await connection.search('ir.module.module', [
['name', '=', args.module_name],
['state', '=', 'installed'],
]);
if (moduleIds.length === 0) {
throw new Error(`Module '${args.module_name}' not found or not installed`);
}
// Uninstall the module
await connection.execute('ir.module.module', 'button_immediate_uninstall', [
moduleIds,
]);
return {
content: [
{
type: 'text',
text: `Module '${args.module_name}' uninstalled successfully`,
},
],
};
}
case 'odoo_upgrade_module': {
const connection = getConnection();
// Find the module
const moduleIds = await connection.search('ir.module.module', [
['name', '=', args.module_name],
['state', '=', 'installed'],
]);
if (moduleIds.length === 0) {
throw new Error(`Module '${args.module_name}' not found or not installed`);
}
// Upgrade the module
await connection.execute('ir.module.module', 'button_immediate_upgrade', [
moduleIds,
]);
return {
content: [
{
type: 'text',
text: `Module '${args.module_name}' upgraded successfully`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Odoo MCP Server running on stdio');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});