|
45 | 45 |
|
46 | 46 |
|
47 | 47 | class MCPMessageType(Enum): |
48 | | - """MCP protocol message types""" |
| 48 | + """MCP protocol message types. |
| 49 | +
|
| 50 | + Enumerates the JSON-RPC methods defined by the Model Context Protocol |
| 51 | + specification. Each value corresponds to a method string sent in the |
| 52 | + ``"method"`` field of an MCP JSON-RPC 2.0 request. |
| 53 | +
|
| 54 | + Attributes: |
| 55 | + TOOLS_LIST: List available tools on the server. |
| 56 | + TOOLS_CALL: Invoke a registered tool by name. |
| 57 | + RESOURCES_LIST: List available resources (files, databases, etc.). |
| 58 | + RESOURCES_READ: Read the contents of a specific resource URI. |
| 59 | + PROMPTS_LIST: List available prompt templates. |
| 60 | + PROMPTS_GET: Retrieve a specific prompt template by name. |
| 61 | + COMPLETION: Request a completion (reserved for future use). |
| 62 | +
|
| 63 | + Example: |
| 64 | + >>> msg_type = MCPMessageType.TOOLS_CALL |
| 65 | + >>> msg_type.value |
| 66 | + 'tools/call' |
| 67 | + """ |
49 | 68 | TOOLS_LIST = "tools/list" |
50 | 69 | TOOLS_CALL = "tools/call" |
51 | 70 | RESOURCES_LIST = "resources/list" |
@@ -75,19 +94,60 @@ class MCPMessageType(Enum): |
75 | 94 |
|
76 | 95 |
|
77 | 96 | class MCPAdapter: |
78 | | - """ |
79 | | - MCP Protocol Adapter with Agent Control Plane Governance. |
80 | | - |
81 | | - This adapter intercepts MCP protocol messages and applies governance |
82 | | - rules before forwarding to the actual MCP server or client. |
83 | | - |
84 | | - MCP uses JSON-RPC 2.0 for communication, with specific methods like: |
85 | | - - tools/list: List available tools |
86 | | - - tools/call: Execute a tool |
87 | | - - resources/list: List available resources |
88 | | - - resources/read: Read a resource |
89 | | - |
90 | | - The adapter ensures all operations respect agent permissions. |
| 97 | + """MCP Protocol Adapter with Agent Control Plane Governance. |
| 98 | +
|
| 99 | + Intercepts MCP protocol messages and applies governance rules before |
| 100 | + forwarding to the actual MCP server or client. MCP uses JSON-RPC 2.0 |
| 101 | + for communication, with specific methods like: |
| 102 | +
|
| 103 | + - ``tools/list``: List available tools |
| 104 | + - ``tools/call``: Execute a tool |
| 105 | + - ``resources/list``: List available resources |
| 106 | + - ``resources/read``: Read a resource |
| 107 | +
|
| 108 | + The adapter ensures all operations respect agent permissions and |
| 109 | + policies defined in the control plane. Unknown tools are denied by |
| 110 | + default (secure-by-default). |
| 111 | +
|
| 112 | + Args: |
| 113 | + control_plane: The ``AgentControlPlane`` instance for governance. |
| 114 | + agent_context: The ``AgentContext`` for the agent using this adapter. |
| 115 | + mcp_handler: Optional upstream MCP message handler to delegate to |
| 116 | + after governance checks pass. |
| 117 | + tool_mapping: Optional custom mapping from tool names to |
| 118 | + ``ActionType`` values. Merged with ``DEFAULT_MCP_MAPPING``. |
| 119 | + on_block: Optional callback invoked when an action is blocked. |
| 120 | + Receives ``(tool_name, arguments, check_result)``. |
| 121 | + logger: Optional logger instance. |
| 122 | +
|
| 123 | + Attributes: |
| 124 | + registered_tools: Dictionary of tool name to tool metadata. |
| 125 | + registered_resources: Dictionary of URI pattern to resource metadata. |
| 126 | + tool_mapping: Combined mapping of tool/operation names to |
| 127 | + ``ActionType`` values used for governance decisions. |
| 128 | +
|
| 129 | + Example: |
| 130 | + >>> from agent_control_plane import AgentControlPlane |
| 131 | + >>> from agent_control_plane.mcp_adapter import MCPAdapter |
| 132 | + >>> |
| 133 | + >>> cp = AgentControlPlane() |
| 134 | + >>> ctx = cp.create_agent("my-agent") |
| 135 | + >>> adapter = MCPAdapter(control_plane=cp, agent_context=ctx) |
| 136 | + >>> |
| 137 | + >>> # Register a tool |
| 138 | + >>> adapter.register_tool("read_file", { |
| 139 | + ... "name": "read_file", |
| 140 | + ... "description": "Read a file from disk", |
| 141 | + ... "inputSchema": {"type": "object", "properties": {"path": {"type": "string"}}} |
| 142 | + ... }) |
| 143 | + >>> |
| 144 | + >>> # Handle an MCP request — governance is applied automatically |
| 145 | + >>> response = adapter.handle_message({ |
| 146 | + ... "jsonrpc": "2.0", |
| 147 | + ... "id": 1, |
| 148 | + ... "method": "tools/call", |
| 149 | + ... "params": {"name": "read_file", "arguments": {"path": "/tmp/data.txt"}} |
| 150 | + ... }) |
91 | 151 | """ |
92 | 152 |
|
93 | 153 | def __init__( |
@@ -130,17 +190,24 @@ def __init__( |
130 | 190 | ) |
131 | 191 |
|
132 | 192 | def handle_message(self, message: Dict[str, Any]) -> Dict[str, Any]: |
133 | | - """ |
134 | | - Handle an MCP protocol message with governance. |
135 | | - |
| 193 | + """Handle an MCP protocol message with governance. |
| 194 | +
|
136 | 195 | This is the main entry point for MCP messages. It parses the |
137 | | - JSON-RPC message, applies governance, and returns the result. |
138 | | - |
| 196 | + JSON-RPC message, applies governance checks via the control plane, |
| 197 | + and returns the result. |
| 198 | +
|
139 | 199 | Args: |
140 | | - message: MCP JSON-RPC 2.0 message |
141 | | - |
| 200 | + message: MCP JSON-RPC 2.0 message containing ``jsonrpc``, |
| 201 | + ``method``, ``params``, and ``id`` fields. |
| 202 | +
|
142 | 203 | Returns: |
143 | | - JSON-RPC 2.0 response |
| 204 | + A JSON-RPC 2.0 response dict. On success, contains a |
| 205 | + ``"result"`` key. On failure (governance block or error), |
| 206 | + contains an ``"error"`` key with ``"code"`` and ``"message"``. |
| 207 | +
|
| 208 | + Raises: |
| 209 | + PermissionError: Internally raised when governance blocks an |
| 210 | + action; caught and converted to a JSON-RPC error response. |
144 | 211 | """ |
145 | 212 | # Parse the JSON-RPC message |
146 | 213 | jsonrpc = message.get("jsonrpc", "2.0") |
@@ -198,7 +265,22 @@ def _handle_tools_list(self, params: Dict) -> Dict: |
198 | 265 | return {"tools": allowed_tools} |
199 | 266 |
|
200 | 267 | def _handle_tools_call(self, params: Dict) -> Dict: |
201 | | - """Handle tools/call - execute a tool with governance.""" |
| 268 | + """Handle tools/call — execute a tool with governance. |
| 269 | +
|
| 270 | + Maps the tool name to an ``ActionType``, checks permissions via |
| 271 | + the control plane, and either delegates to the registered handler |
| 272 | + or returns the control plane result. |
| 273 | +
|
| 274 | + Args: |
| 275 | + params: JSON-RPC params containing ``"name"`` and ``"arguments"``. |
| 276 | +
|
| 277 | + Returns: |
| 278 | + Tool execution result dict with MCP ``content`` format. |
| 279 | +
|
| 280 | + Raises: |
| 281 | + PermissionError: If the tool is unknown or governance denies |
| 282 | + the action. |
| 283 | + """ |
202 | 284 | tool_name = params.get("name", "") |
203 | 285 | arguments = params.get("arguments", {}) |
204 | 286 |
|
@@ -259,7 +341,20 @@ def _handle_resources_list(self, params: Dict) -> Dict: |
259 | 341 | return {"resources": allowed_resources} |
260 | 342 |
|
261 | 343 | def _handle_resources_read(self, params: Dict) -> Dict: |
262 | | - """Handle resources/read - read a resource with governance.""" |
| 344 | + """Handle resources/read — read a resource with governance. |
| 345 | +
|
| 346 | + Determines the ``ActionType`` from the URI scheme and checks |
| 347 | + permissions before returning resource contents. |
| 348 | +
|
| 349 | + Args: |
| 350 | + params: JSON-RPC params containing ``"uri"``. |
| 351 | +
|
| 352 | + Returns: |
| 353 | + Resource contents dict with MCP ``contents`` format. |
| 354 | +
|
| 355 | + Raises: |
| 356 | + PermissionError: If governance denies the resource read. |
| 357 | + """ |
263 | 358 | uri = params.get("uri", "") |
264 | 359 |
|
265 | 360 | self.logger.info(f"Resource read request: {uri}") |
@@ -321,7 +416,21 @@ def _handle_prompts_get(self, params: Dict) -> Dict: |
321 | 416 | } |
322 | 417 |
|
323 | 418 | def _map_tool_to_action(self, tool_name: str) -> Optional[ActionType]: |
324 | | - """Map an MCP tool name to an ActionType.""" |
| 419 | + """Map an MCP tool name to an ``ActionType``. |
| 420 | +
|
| 421 | + Resolution order: |
| 422 | + 1. Exact match in ``self.tool_mapping`` (case-insensitive). |
| 423 | + 2. Pattern-based heuristics (e.g. names containing ``"read"`` |
| 424 | + and ``"file"`` map to ``FILE_READ``). |
| 425 | + 3. Returns ``None`` for unrecognized tools (deny-by-default). |
| 426 | +
|
| 427 | + Args: |
| 428 | + tool_name: The MCP tool name to resolve. |
| 429 | +
|
| 430 | + Returns: |
| 431 | + The corresponding ``ActionType``, or ``None`` if the tool |
| 432 | + cannot be mapped (triggering a denial). |
| 433 | + """ |
325 | 434 | tool_name_lower = tool_name.lower() |
326 | 435 |
|
327 | 436 | # Check exact match |
@@ -411,11 +520,36 @@ def add_tool_mapping(self, tool_name: str, action_type: ActionType): |
411 | 520 |
|
412 | 521 |
|
413 | 522 | class MCPServer: |
414 | | - """ |
415 | | - Simplified MCP Server with built-in governance. |
416 | | - |
417 | | - This class provides a simple way to create an MCP-compliant server |
418 | | - with Agent Control Plane governance built in. |
| 523 | + """Simplified MCP Server with built-in governance. |
| 524 | +
|
| 525 | + Provides a high-level API for creating an MCP-compliant server with |
| 526 | + Agent Control Plane governance built in. Wraps an ``MCPAdapter`` |
| 527 | + internally and exposes convenience methods for tool and resource |
| 528 | + registration. |
| 529 | +
|
| 530 | + Args: |
| 531 | + server_name: Human-readable name for this MCP server. |
| 532 | + control_plane: ``AgentControlPlane`` instance for governance. |
| 533 | + agent_context: ``AgentContext`` representing the server's agent. |
| 534 | + transport: Transport method — ``"stdio"`` (default) or ``"sse"``. |
| 535 | + logger: Optional logger instance. |
| 536 | +
|
| 537 | + Attributes: |
| 538 | + adapter: The underlying ``MCPAdapter`` that handles governance. |
| 539 | + server_name: Name of this server. |
| 540 | + transport: Active transport method. |
| 541 | +
|
| 542 | + Example: |
| 543 | + >>> from agent_control_plane import AgentControlPlane |
| 544 | + >>> from agent_control_plane.mcp_adapter import MCPServer |
| 545 | + >>> |
| 546 | + >>> cp = AgentControlPlane() |
| 547 | + >>> ctx = cp.create_agent("file-agent") |
| 548 | + >>> server = MCPServer("file-server", cp, ctx, transport="stdio") |
| 549 | + >>> |
| 550 | + >>> server.register_tool("read_file", handle_read, "Read a file") |
| 551 | + >>> server.register_resource("file://", handle_resource, "File resources") |
| 552 | + >>> server.start() # All calls are now governed |
419 | 553 | """ |
420 | 554 |
|
421 | 555 | def __init__( |
|
0 commit comments