diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index c56b6dd..ccf53bc 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "odoo-mpm", - "version": "0.1.2", - "description": "Connects Claude to MPM's Odoo instance (mpmedia.odoo.com) — Products, Knowledge, Contacts, Sales, CRM, Project, Helpdesk, Purchase, Inventory, and Employees.", + "version": "0.2.0", + "description": "Read-only connection to MPM's Odoo instance (mpmedia.odoo.com) — Products, Knowledge, Contacts, Sales, CRM, Project, Helpdesk, Purchase, Inventory, and Employees.", "author": { "name": "Message Point Media" }, "keywords": ["odoo", "erp", "mpm", "project", "crm", "helpdesk"] } diff --git a/server/odoo_mcp.py b/server/odoo_mcp.py index 8698aa0..cfe5dae 100644 --- a/server/odoo_mcp.py +++ b/server/odoo_mcp.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Odoo MCP Server for MPM -Connects to mpmedia.odoo.com via XML-RPC and exposes tools for +Odoo MCP Server for MPM (read-only) +Connects to mpmedia.odoo.com via XML-RPC and exposes read-only tools for Products, Knowledge, Contacts, Sales, CRM, Project, Helpdesk, Purchase, Inventory, Employees, and Knowledge Templates. """ @@ -82,12 +82,6 @@ def _get_credentials() -> tuple[str, str]: api_key = ODOO_API_KEY or _keychain_get("odoo_api_key") return username, api_key -def _get_credentials() -> tuple[str, str]: - """Resolve username and API key: env vars take priority, then macOS Keychain.""" - username = ODOO_USERNAME or _keychain_get("odoo_username") - api_key = ODOO_API_KEY or _keychain_get("odoo_api_key") - return username, api_key - def _connect(): global _uid, _models, _resolved_api_key if _uid is not None: @@ -132,11 +126,6 @@ def _read(model, ids, fields=None): kw["fields"] = fields return _call(model, "read", [ids], kw) -def _create(model, vals): - return _call(model, "create", [vals]) - -def _write(model, ids, vals): - return _call(model, "write", [[ids] if isinstance(ids, int) else ids, vals]) # ── FastMCP App ─────────────────────────────────────────────────────────────── mcp = FastMCP("Odoo MPM") @@ -274,26 +263,6 @@ def get_knowledge_article(article_id: int) -> dict: ) return article -@mcp.tool() -def create_knowledge_article(name: str, body: str, parent_id: int = None) -> int: - """Create a new Knowledge article. body is HTML. Returns new article ID.""" - vals = {"name": name, "body": body} - if parent_id: - vals["parent_id"] = parent_id - return _create("knowledge.article", vals) - -@mcp.tool() -def update_knowledge_article(article_id: int, name: str = "", body: str = "") -> bool: - """Update a Knowledge article's title and/or body (HTML).""" - vals = {} - if name: - vals["name"] = name - if body: - vals["body"] = body - if not vals: - return False - return _write("knowledge.article", article_id, vals) - @mcp.tool() def search_knowledge_templates(query: str = "", category: str = "", limit: int = 50) -> list: """Search Knowledge Base article templates. @@ -375,18 +344,6 @@ def get_contact(contact_id: int) -> dict: "website", "comment", "category_id", "child_ids", "user_id"]) return r[0] if r else {} -@mcp.tool() -def create_contact(name: str, email: str = "", phone: str = "", - is_company: bool = False, parent_id: int = None, - street: str = "", city: str = "") -> int: - """Create a new contact. Returns the new contact ID.""" - vals = {"name": name, "is_company": is_company} - if email: vals["email"] = email - if phone: vals["phone"] = phone - if parent_id: vals["parent_id"] = parent_id - if street: vals["street"] = street - if city: vals["city"] = city - return _create("res.partner", vals) # ════════════════════════════════════════════════════════════════════════════ @@ -425,19 +382,6 @@ def get_sales_order(order_id: int) -> dict: order["lines"] = lines return order -@mcp.tool() -def create_sales_order(partner_id: int, lines: list) -> int: - """Create a sales order. lines is a list of dicts with keys: - product_id (int), product_uom_qty (float), price_unit (float). - Returns new order ID.""" - order_lines = [] - for l in lines: - order_lines.append((0, 0, { - "product_id": l["product_id"], - "product_uom_qty": l.get("product_uom_qty", 1), - "price_unit": l.get("price_unit", 0), - })) - return _create("sale.order", {"partner_id": partner_id, "order_line": order_lines}) # ════════════════════════════════════════════════════════════════════════════ @@ -470,28 +414,6 @@ def get_crm_lead(lead_id: int) -> dict: "activity_ids", "date_conversion"]) return r[0] if r else {} -@mcp.tool() -def create_crm_lead(name: str, partner_id: int = None, expected_revenue: float = 0, - description: str = "", email: str = "", phone: str = "") -> int: - """Create a new CRM opportunity. Returns new lead ID.""" - vals = {"name": name, "type": "opportunity", "expected_revenue": expected_revenue} - if partner_id: vals["partner_id"] = partner_id - if description: vals["description"] = description - if email: vals["email_from"] = email - if phone: vals["phone"] = phone - return _create("crm.lead", vals) - -@mcp.tool() -def update_crm_lead(lead_id: int, stage_id: int = None, probability: float = None, - expected_revenue: float = None, note: str = "") -> bool: - """Update a CRM lead's stage, probability, revenue, or notes.""" - vals = {} - if stage_id is not None: vals["stage_id"] = stage_id - if probability is not None: vals["probability"] = probability - if expected_revenue is not None: vals["expected_revenue"] = expected_revenue - if note: vals["description"] = note - return _write("crm.lead", lead_id, vals) if vals else False - @mcp.tool() def list_crm_stages() -> list: """List all CRM pipeline stages with their IDs and names.""" @@ -543,29 +465,6 @@ def get_task(task_id: int) -> dict: "child_ids", "depend_on_ids", "planned_hours", "effective_hours"]) return r[0] if r else {} -@mcp.tool() -def create_task(name: str, project_id: int, description: str = "", - date_deadline: str = "", user_ids: list = None) -> int: - """Create a project task. date_deadline format: YYYY-MM-DD. Returns task ID.""" - vals = {"name": name, "project_id": project_id} - if description: vals["description"] = description - if date_deadline: vals["date_deadline"] = date_deadline - if user_ids: vals["user_ids"] = [(6, 0, user_ids)] - return _create("project.task", vals) - -@mcp.tool() -def update_task(task_id: int, name: str = "", stage_id: int = None, - description: str = "", date_deadline: str = "", - kanban_state: str = "") -> bool: - """Update a task. kanban_state: normal, done, blocked.""" - vals = {} - if name: vals["name"] = name - if stage_id: vals["stage_id"] = stage_id - if description: vals["description"] = description - if date_deadline: vals["date_deadline"] = date_deadline - if kanban_state: vals["kanban_state"] = kanban_state - return _write("project.task", task_id, vals) if vals else False - @mcp.tool() def list_task_stages(project_id: int = None) -> list: """List task stages. Optionally filter by project.""" @@ -613,26 +512,6 @@ def get_helpdesk_ticket(ticket_id: int) -> dict: "date_last_stage_update", "kanban_state", "tag_ids"]) return r[0] if r else {} -@mcp.tool() -def create_helpdesk_ticket(name: str, description: str = "", - partner_id: int = None, team_id: int = None) -> int: - """Create a helpdesk ticket. Returns new ticket ID.""" - vals = {"name": name} - if description: vals["description"] = description - if partner_id: vals["partner_id"] = partner_id - if team_id: vals["team_id"] = team_id - return _create("helpdesk.ticket", vals) - -@mcp.tool() -def update_helpdesk_ticket(ticket_id: int, stage_id: int = None, - user_id: int = None, note: str = "") -> bool: - """Update a helpdesk ticket's stage, assignee, or add a note.""" - vals = {} - if stage_id: vals["stage_id"] = stage_id - if user_id: vals["user_id"] = user_id - if note: vals["description"] = note - return _write("helpdesk.ticket", ticket_id, vals) if vals else False - @mcp.tool() def list_helpdesk_teams() -> list: """List all helpdesk teams."""