Files
odoo-ai/agent_service/mcp/tools.py
ActiveBlue Build 66b114cdcf feat(mcp): add MCP gateway — 14 tools over SSE, all agent calls forced local
Architecture:
- agent_service/mcp/tools.py: 14 Tool definitions with JSON schemas
    dispatch, finance_query, accounting_query, crm_query, sales_query,
    project_query, elearning_query, expenses_query, employees_query,
    get_health, list_agents, trigger_sweep, get_pending_approvals, approve_directive
- agent_service/mcp/server.py: mcp.Server with list_tools + call_tool handlers
- agent_service/routers/mcp_router.py: Starlette routes at /mcp/sse + /mcp/messages
- main.py: mounts MCP routes alongside existing FastAPI routers (graceful fallback if mcp not installed)

Privacy guarantee (enforced in server.py, not by convention):
- _force_local_context() sets llm_router._privacy_mode = 'local' before EVERY agent call
- _restore_mode() restores original mode after the tool returns
- HIPAA agents (finance, accounting, expenses, employees) were already Ollama-only;
  MCP adds a second enforcement layer for all 8 agents
- MCP client (e.g. Claude Code CLI) receives only tool results — no LLM completions cross the boundary

Usage (Claude Code CLI):
  claude mcp add --transport sse http://192.168.2.47:8001/mcp/sse
  or copy claude_mcp_config.json to ~/.claude/mcp_servers.json

requirements.txt: added mcp==1.3.0
tests/test_mcp_server.py: 13 tests covering tool count, schemas, HIPAA labelling, privacy override

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 16:45:49 -04:00

248 lines
9.9 KiB
Python

"""
MCP tool definitions for ActiveBlue AI.
All 14 tools are exposed here. Every tool that invokes an agent
is FORCED to use the local Ollama LLM regardless of the global
privacy_mode setting. This is enforced in server.py before any
agent.handle_message() or agent.sweep() call.
Tools:
dispatch → MasterAgent (routes to best specialist agents)
finance_query → FinanceAgent (HIPAA-locked, Ollama only)
accounting_query → AccountingAgent (HIPAA-locked, Ollama only)
crm_query → CrmAgent
sales_query → SalesAgent
project_query → ProjectAgent
elearning_query → ElearningAgent
expenses_query → ExpensesAgent (HIPAA-locked, Ollama only)
employees_query → EmployeesAgent (HIPAA-locked, Ollama only)
get_health → FastAPI /health/detailed proxy
list_agents → AgentRegistry listing
trigger_sweep → SweepCoordinator
get_pending_approvals → ab_directive_log pending_approval rows
approve_directive → approve or reject a pending directive
"""
from mcp.types import Tool
MCP_TOOLS: list[Tool] = [
Tool(
name='dispatch',
description=(
'Send a natural-language message to the ActiveBlue AI MasterAgent. '
'The master agent classifies intent and routes to the relevant specialist '
'agents (finance, CRM, sales, project, etc.) running on local Ollama. '
'Returns a synthesised reply with agent reports, escalations, and actions taken.'
),
inputSchema={
'type': 'object',
'properties': {
'message': {
'type': 'string',
'description': 'The natural-language request or question',
},
'user_id': {
'type': 'string',
'description': 'User identifier for memory scoping (default: mcp_user)',
'default': 'mcp_user',
},
'context': {
'type': 'object',
'description': 'Optional context dict (partner_id, project_id, etc.)',
'default': {},
},
},
'required': ['message'],
},
),
Tool(
name='finance_query',
description=(
'Query the Finance Agent directly. Analyses invoices, overdue balances, '
'collection rates, and payment history. Can send payment reminders and flag '
'records for review. HIPAA-locked: always uses local Ollama.'
),
inputSchema={
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'Finance question or instruction'},
'partner_id': {'type': 'integer', 'description': 'Filter by Odoo partner ID'},
'period': {'type': 'string', 'description': 'Period filter e.g. "this_month", "last_quarter"'},
},
'required': ['query'],
},
),
Tool(
name='accounting_query',
description=(
'Query the Accounting Agent directly. Analyses trial balance, chart of accounts, '
'journal entries, and tax position. Read-only; never posts entries. '
'HIPAA-locked: always uses local Ollama.'
),
inputSchema={
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'Accounting question or report request'},
'date_from': {'type': 'string', 'description': 'ISO date YYYY-MM-DD'},
'date_to': {'type': 'string', 'description': 'ISO date YYYY-MM-DD'},
},
'required': ['query'],
},
),
Tool(
name='crm_query',
description=(
'Query the CRM Agent directly. Analyses pipeline, opportunities, leads, '
'won/lost analysis. Can move stages and log activities. Uses local Ollama.'
),
inputSchema={
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'CRM question or instruction'},
'user_id': {'type': 'integer', 'description': 'Filter by salesperson Odoo user ID'},
},
'required': ['query'],
},
),
Tool(
name='sales_query',
description=(
'Query the Sales Agent directly. Analyses sales orders, quotations, '
'revenue by rep, expired quotes. Uses local Ollama.'
),
inputSchema={
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'Sales question or instruction'},
'partner_id': {'type': 'integer', 'description': 'Filter by customer partner ID'},
'date_from': {'type': 'string', 'description': 'ISO date YYYY-MM-DD'},
'date_to': {'type': 'string', 'description': 'ISO date YYYY-MM-DD'},
},
'required': ['query'],
},
),
Tool(
name='project_query',
description=(
'Query the Project Agent directly. Analyses tasks, blocked items, overdue '
'deadlines, and timesheets. Can create tasks and assign them. Uses local Ollama.'
),
inputSchema={
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'Project question or instruction'},
'project_id': {'type': 'integer', 'description': 'Filter by Odoo project ID'},
},
'required': ['query'],
},
),
Tool(
name='elearning_query',
description=(
'Query the eLearning Agent directly. Analyses course completion rates, '
'enrolled users, low-engagement courses, and suggests next courses. Uses local Ollama.'
),
inputSchema={
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'eLearning question or instruction'},
'channel_id': {'type': 'integer', 'description': 'Filter by slide.channel ID'},
'partner_id': {'type': 'integer', 'description': 'Learner partner ID'},
},
'required': ['query'],
},
),
Tool(
name='expenses_query',
description=(
'Query the Expenses Agent directly. Analyses expense reports, pending approvals, '
'policy violations. HIPAA-locked: always uses local Ollama.'
),
inputSchema={
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'Expenses question or instruction'},
'employee_id': {'type': 'integer', 'description': 'Filter by Odoo employee ID'},
},
'required': ['query'],
},
),
Tool(
name='employees_query',
description=(
'Query the Employees (HR) Agent directly. Analyses headcount, contracts, '
'leave requests, and attendance. HIPAA-locked: always uses local Ollama. '
'Salary data is never returned to the caller.'
),
inputSchema={
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'HR question or instruction'},
'department_id': {'type': 'integer', 'description': 'Filter by department ID'},
'employee_id': {'type': 'integer', 'description': 'Filter by employee ID'},
},
'required': ['query'],
},
),
Tool(
name='get_health',
description='Get detailed health status of the ActiveBlue AI service including DB, Odoo, and Ollama connectivity.',
inputSchema={'type': 'object', 'properties': {}, 'required': []},
),
Tool(
name='list_agents',
description='List all registered AI agents, their domains, active status, and current LLM backend.',
inputSchema={'type': 'object', 'properties': {}, 'required': []},
),
Tool(
name='trigger_sweep',
description=(
'Trigger a proactive sweep across all active agents (or a specific subset). '
'Agents check for overdue invoices, blocked tasks, expired contracts, etc. '
'Returns findings and actions taken.'
),
inputSchema={
'type': 'object',
'properties': {
'agents': {
'type': 'array',
'items': {'type': 'string'},
'description': 'Agent names to sweep. Empty = all active agents.',
'default': [],
},
},
'required': [],
},
),
Tool(
name='get_pending_approvals',
description='List AI directives that are waiting for human approval before proceeding.',
inputSchema={'type': 'object', 'properties': {}, 'required': []},
),
Tool(
name='approve_directive',
description='Approve or reject a pending AI directive. Rejected directives are cancelled.',
inputSchema={
'type': 'object',
'properties': {
'directive_id': {'type': 'string', 'description': 'The directive UUID to respond to'},
'approved': {'type': 'boolean', 'description': 'True to approve, False to reject'},
'note': {'type': 'string', 'description': 'Optional reason or note'},
},
'required': ['directive_id', 'approved'],
},
),
]
# Map tool name → agent name for the 8 specialist agent tools
AGENT_TOOL_MAP: dict[str, str] = {
'finance_query': 'finance_agent',
'accounting_query': 'accounting_agent',
'crm_query': 'crm_agent',
'sales_query': 'sales_agent',
'project_query': 'project_agent',
'elearning_query': 'elearning_agent',
'expenses_query': 'expenses_agent',
'employees_query': 'employees_agent',
}