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>
This commit is contained in:
247
agent_service/mcp/tools.py
Normal file
247
agent_service/mcp/tools.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
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',
|
||||
}
|
||||
Reference in New Issue
Block a user