feat: MCP Server plugin (Task 5.16) - expose LeoCRM tools to external AI clients

- New plugin app/plugins/builtins/mcp_server/ with 9 MCP tools
- Tools: search_contacts, get_contact, create_contact, list_calendar_entries,
  create_calendar_entry, list_emails, send_email, list_files, upload_file
- Routes: GET /api/v1/mcp/tools, POST /api/v1/mcp/tools/{name}/execute, GET /api/v1/mcp/config
- API-token auth via session + RBAC (mcp:read, mcp:write)
- Frontend: mcp.ts API client with React Query hooks
- Frontend: SettingsMcp.tsx settings page with tool listing and execution
- i18n: de.json and en.json updated with MCP entries
- Tests: 7 tests covering tool listing, config, execution, auth, schema validation
This commit is contained in:
Agent Zero
2026-07-23 23:01:59 +02:00
parent f4beb78f91
commit 9d4f701a25
14 changed files with 1313 additions and 6 deletions
+61
View File
@@ -38,6 +38,9 @@ from app.models.user import User, UserTenant
from app.models.user_preference import UserPreference # noqa: F401
from app.models.workflow import Workflow, WorkflowInstance, WorkflowStepHistory # noqa: F401
from app.plugins.builtins.calendar import CalendarPlugin # noqa: F401
from app.plugins.builtins.mcp_server import McpServerPlugin # noqa: F401
from app.plugins.builtins.mcp_client import McpClientPlugin # noqa: F401
from app.plugins.builtins.mcp_client.models import McpServerConfig # noqa: F401
from app.plugins.builtins.calendar.models import ( # noqa: F401
Calendar,
CalendarEntry,
@@ -434,3 +437,61 @@ async def calendar_authed_client(
assert resp.status_code == 200, f"Calendar activate failed: {resp.text}"
return calendar_client, seed
# ─── MCP Server / Client Fixtures ───────────────────────────────────────────
@pytest_asyncio.fixture
async def mcp_app(engine: AsyncEngine, redis_client):
"""FastAPI app with MCP Server + Client + Permissions plugins registered, installed, and activated."""
reset_engine_for_testing(engine)
app = create_app()
registry = reset_registry_for_testing()
registry.initialize(engine, app)
container = get_container()
await container.initialize()
registry.register_plugin(PermissionsPlugin())
registry.register_plugin(McpServerPlugin())
registry.register_plugin(McpClientPlugin())
reset_plugin_service_for_testing(registry)
_sf = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
async with _sf() as session:
await registry.install(session, "permissions")
await registry.activate(session, "permissions")
await registry.install(session, "mcp_server")
await registry.activate(session, "mcp_server")
await registry.install(session, "mcp_client")
await registry.activate(session, "mcp_client")
await session.commit()
yield app
await close_engine()
@pytest_asyncio.fixture
async def mcp_client_fixture(mcp_app) -> AsyncClient:
transport = ASGITransport(app=mcp_app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest_asyncio.fixture
async def mcp_authed_client(
mcp_client_fixture: AsyncClient, db_session: AsyncSession
) -> tuple[AsyncClient, dict]:
"""Authenticated admin client with seeded data and MCP plugins activated."""
seed = await seed_tenant_and_users(db_session)
login_resp = await mcp_client_fixture.post(
"/api/v1/auth/login",
json={"email": "admin@tenanta.com", "password": "TestPass123!"},
headers=ORIGIN_HEADER,
)
assert login_resp.status_code == 200, f"Login failed: {login_resp.text}"
csrf_token = login_resp.json().get("csrf_token", "")
mcp_client_fixture.headers.update({"X-CSRF-Token": csrf_token})
return mcp_client_fixture, seed