From e24a87db6fe0db172ac7a36516c6d033d8256b3467e0469327164508ee668edf Mon Sep 17 00:00:00 2001 From: Tyler J King Date: Tue, 14 Apr 2026 05:25:08 -0400 Subject: [PATCH] feat(mcp): add Intune device management tools MCP tools for list_devices, get_device_compliance, sync_device, remote_lock. All route through governed IntuneConnector invocation with Chronicle audit. Signed-off-by: Tyler King --- gsap_broker/mcp.py | 73 ++++++++++++++++++++++++++++++++++++++++ tests/test_mcp_intune.py | 48 ++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 tests/test_mcp_intune.py diff --git a/gsap_broker/mcp.py b/gsap_broker/mcp.py index 615758a..980a66e 100644 --- a/gsap_broker/mcp.py +++ b/gsap_broker/mcp.py @@ -158,6 +158,49 @@ TOOLS = [ "description": "Get current session details: principal, AC scope, delegation, DEFCON level.", "inputSchema": {"type": "object", "properties": {}}, }, + { + "name": "list_devices", + "description": "List managed devices from Intune. Requires Intune connector enabled.", + "inputSchema": { + "type": "object", + "properties": { + "top": {"type": "integer", "description": "Max devices to return (default: 50)"}, + }, + }, + }, + { + "name": "get_device_compliance", + "description": "Check compliance state of a specific device via Intune.", + "inputSchema": { + "type": "object", + "properties": { + "device_id": {"type": "string", "description": "Intune managed device ID"}, + }, + "required": ["device_id"], + }, + }, + { + "name": "sync_device", + "description": "Trigger Intune sync for a device. Requires PROPOSE capability.", + "inputSchema": { + "type": "object", + "properties": { + "device_id": {"type": "string", "description": "Intune managed device ID"}, + }, + "required": ["device_id"], + }, + }, + { + "name": "remote_lock", + "description": "Remote lock a managed device. Requires MUTATE capability. May require ceremony approval in production Accords.", + "inputSchema": { + "type": "object", + "properties": { + "device_id": {"type": "string", "description": "Intune managed device ID"}, + }, + "required": ["device_id"], + }, + }, ] @@ -411,6 +454,32 @@ async def _handle_session_info(request: Request) -> dict: } +async def _handle_intune_tool(tool_name: str, args: dict) -> dict: + """Route Intune MCP tools through the governed IntuneConnector.""" + from gsap_broker.routers.connectors import _registry + from gsap_broker.connectors.base import ConnectorContext + + intune = _registry.get("intune") + if intune is None: + return {"error": "Intune connector not enabled. Set intune_enabled=True."} + + op_map = { + "list_devices": "list_devices", + "get_device_compliance": "get_compliance", + "sync_device": "sync_device", + "remote_lock": "remote_lock", + } + operation = op_map.get(tool_name) + if not operation: + return {"error": f"Unknown Intune tool: {tool_name}"} + + ctx = ConnectorContext(gsap_context_id=args.get("ac_id", "mcp-session")) + result = await intune.invoke(operation, args, ctx) + if result.success: + return {"data": result.data, "lineage_cid": result.lineage_cid} + return {"error": result.error} + + # ── Tool Dispatch ──────────────────────────────────────────────── @@ -427,6 +496,10 @@ async def _dispatch_tool(request: Request, tool_name: str, arguments: dict) -> d "get_posture": lambda: _handle_get_posture(arguments), "check_operation": lambda: _handle_check_operation(arguments, request), "session_info": lambda: _handle_session_info(request), + "list_devices": lambda: _handle_intune_tool(tool_name, arguments), + "get_device_compliance": lambda: _handle_intune_tool(tool_name, arguments), + "sync_device": lambda: _handle_intune_tool(tool_name, arguments), + "remote_lock": lambda: _handle_intune_tool(tool_name, arguments), } handler = handlers.get(tool_name) diff --git a/tests/test_mcp_intune.py b/tests/test_mcp_intune.py new file mode 100644 index 0000000..00c0f07 --- /dev/null +++ b/tests/test_mcp_intune.py @@ -0,0 +1,48 @@ +# Copyright 2026 Guildhouse Dev +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Intune MCP tools.""" + +import pytest +from httpx import AsyncClient, ASGITransport +from gsap_broker.app import app + + +@pytest.fixture +async def client(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: + yield c + + +@pytest.mark.asyncio +async def test_mcp_tools_list_includes_intune(client): + """MCP tools/list should include Intune tools.""" + resp = await client.post("/mcp", json={ + "jsonrpc": "2.0", + "method": "tools/list", + "id": 1, + }) + assert resp.status_code == 200 + tools = resp.json()["result"]["tools"] + tool_names = [t["name"] for t in tools] + assert "list_devices" in tool_names + assert "get_device_compliance" in tool_names + assert "sync_device" in tool_names + assert "remote_lock" in tool_names + + +@pytest.mark.asyncio +async def test_mcp_intune_tool_without_connector(client): + """Intune MCP tool should return error when connector not enabled.""" + resp = await client.post("/mcp", json={ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "list_devices", + "arguments": {}, + }, + "id": 2, + }) + assert resp.status_code == 200 + content = resp.json()["result"]["content"][0]["text"] + assert "not enabled" in content.lower() or "error" in content.lower()