|
4 | 4 | from guillotina.contrib.mcp.interfaces import IMCPToolRegistry |
5 | 5 | from guillotina.contrib.mcp.security import require_access_content |
6 | 6 | from guillotina.interfaces import IResource |
7 | | -from guillotina.response import HTTPNotFound, HTTPServiceUnavailable, Response |
| 7 | +from guillotina.response import HTTPMethodNotAllowed, HTTPNotFound, HTTPServiceUnavailable, Response |
| 8 | + |
| 9 | + |
| 10 | +_MCP_PROTOCOL_POST_METHODS = ["POST"] |
| 11 | + |
| 12 | + |
| 13 | +def _reject_non_post_protocol(context, request, method: str): |
| 14 | + action = request.matchdict.get("action", "") |
| 15 | + if action != "protocol": |
| 16 | + raise HTTPNotFound(content={"reason": f"Unknown MCP {method} action: {action}"}) |
| 17 | + require_access_content(context) |
| 18 | + if method == "DELETE": |
| 19 | + reason = "MCP endpoint does not support session termination" |
| 20 | + else: |
| 21 | + reason = "MCP endpoint does not offer an SSE stream" |
| 22 | + raise HTTPMethodNotAllowed( |
| 23 | + method, |
| 24 | + _MCP_PROTOCOL_POST_METHODS, |
| 25 | + content={"reason": reason}, |
| 26 | + ) |
8 | 27 |
|
9 | 28 |
|
10 | 29 | def _get_registry(): |
@@ -72,3 +91,29 @@ async def _handle_protocol(self): |
72 | 91 | resp._prepared = True |
73 | 92 | resp._eof_sent = True |
74 | 93 | return resp |
| 94 | + |
| 95 | + |
| 96 | +@configure.service( |
| 97 | + method="GET", |
| 98 | + context=IResource, |
| 99 | + name="@mcp/{action}", |
| 100 | + permission="guillotina.MCPExecute", |
| 101 | + summary="MCP Streamable HTTP GET is not offered (JSON-only)", |
| 102 | + allow_access=True, |
| 103 | +) |
| 104 | +class MCPActionGetService(Service): |
| 105 | + async def __call__(self): |
| 106 | + _reject_non_post_protocol(self.context, self.request, "GET") |
| 107 | + |
| 108 | + |
| 109 | +@configure.service( |
| 110 | + method="DELETE", |
| 111 | + context=IResource, |
| 112 | + name="@mcp/{action}", |
| 113 | + permission="guillotina.MCPExecute", |
| 114 | + summary="MCP Streamable HTTP session termination is not offered", |
| 115 | + allow_access=True, |
| 116 | +) |
| 117 | +class MCPActionDeleteService(Service): |
| 118 | + async def __call__(self): |
| 119 | + _reject_non_post_protocol(self.context, self.request, "DELETE") |
0 commit comments