Skip to content

Commit 73a1532

Browse files
Teryl Taylorcrivetimihai
authored andcommitted
fix: json mcp error codes and added test cases.
Signed-off-by: Teryl Taylor <[email protected]>
1 parent 92b1eba commit 73a1532

File tree

2 files changed

+274
-19
lines changed

2 files changed

+274
-19
lines changed

mcpgateway/main.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@
7272
from mcpgateway.middleware.request_logging_middleware import RequestLoggingMiddleware
7373
from mcpgateway.middleware.security_headers import SecurityHeadersMiddleware
7474
from mcpgateway.middleware.token_scoping import token_scoping_middleware
75-
from mcpgateway.models import InitializeResult, ListResourceTemplatesResult, LogLevel, Root
75+
from mcpgateway.models import InitializeResult
76+
from mcpgateway.models import JSONRPCError as PydanticJSONRPCError
77+
from mcpgateway.models import ListResourceTemplatesResult, LogLevel, Root
7678
from mcpgateway.observability import init_telemetry
7779
from mcpgateway.plugins.framework import PluginError, PluginManager, PluginViolationError
7880
from mcpgateway.routers.well_known import router as well_known_router
@@ -701,32 +703,50 @@ async def plugin_violation_exception_handler(_request: Request, exc: PluginViola
701703
exc: The PluginViolationError exception containing constraint
702704
violation details.
703705
704-
Raises:
705-
JSONRPCError: A -32602 JSON RPC error, which indicates invalid parameters.
706+
Returns:
707+
JSONResponse: A 200 response with error details in JSON-RPC format.
706708
707709
Examples:
708710
>>> from mcpgateway.plugins.framework import PluginViolationError
709711
>>> from mcpgateway.plugins.framework.models import PluginViolation
710712
>>> from fastapi import Request
711713
>>> import asyncio
714+
>>> import json
712715
>>>
713-
>>> # Create a mock integrity error
716+
>>> # Create a plugin violation error
714717
>>> mock_error = PluginViolationError(message="plugin violation",violation = PluginViolation(
715718
... reason="Invalid input",
716719
... description="The input contains prohibited content",
717720
... code="PROHIBITED_CONTENT",
718721
... details={"field": "message", "value": "test"}
719722
... ))
720-
>>> asyncio.run(plugin_violation_exception_handler(None, mock_error))
721-
Traceback (most recent call last):
722-
...
723-
mcpgateway.validation.jsonrpc.JSONRPCError: The input contains prohibited content
723+
>>> result = asyncio.run(plugin_violation_exception_handler(None, mock_error))
724+
>>> result.status_code
725+
200
726+
>>> content = json.loads(result.body.decode())
727+
>>> content["error"]["code"]
728+
-32602
729+
>>> "Plugin Violation:" in content["error"]["message"]
730+
True
731+
>>> content["error"]["data"]["plugin_error_code"]
732+
'PROHIBITED_CONTENT'
724733
"""
725734
policy_violation = exc.violation.model_dump() if exc.violation else {}
726735
message = exc.violation.description if exc.violation else "A plugin violation occurred."
727736
policy_violation["message"] = exc.message
728737
status_code = exc.violation.mcp_error_code if exc.violation and exc.violation.mcp_error_code else -32602
729-
raise JSONRPCError(code=status_code, message=message, data=policy_violation)
738+
violation_details: dict[str, Any] = {}
739+
if exc.violation:
740+
if exc.violation.description:
741+
violation_details["description"] = exc.violation.description
742+
if exc.violation.details:
743+
violation_details["details"] = exc.violation.details
744+
if exc.violation.code:
745+
violation_details["plugin_error_code"] = exc.violation.code
746+
if exc.violation.plugin_name:
747+
violation_details["plugin_name"] = exc.violation.plugin_name
748+
json_rpc_error = PydanticJSONRPCError(code=status_code, message="Plugin Violation: " + message, data=violation_details)
749+
return JSONResponse(status_code=200, content={"error": json_rpc_error.model_dump()})
730750

731751

732752
@app.exception_handler(PluginError)
@@ -742,31 +762,48 @@ async def plugin_exception_handler(_request: Request, exc: PluginError):
742762
exc: The PluginError exception containing constraint
743763
violation details.
744764
745-
Raises:
746-
JSONRPCError: A -32603 JSON RPC error internal server error.
765+
Returns:
766+
JSONResponse: A 200 response with error details in JSON-RPC format.
747767
748768
Examples:
749-
>>> from mcpgateway.plugins.framework import PluginViolationError
769+
>>> from mcpgateway.plugins.framework import PluginError
750770
>>> from mcpgateway.plugins.framework.models import PluginErrorModel
751771
>>> from fastapi import Request
752772
>>> import asyncio
773+
>>> import json
753774
>>>
754-
>>> # Create a mock integrity error
775+
>>> # Create a plugin error
755776
>>> mock_error = PluginError(error = PluginErrorModel(
756777
... message="plugin error",
757778
... code="timeout",
758779
... plugin_name="abc",
759780
... details={"field": "message", "value": "test"}
760781
... ))
761-
>>> asyncio.run(plugin_exception_handler(None, mock_error))
762-
Traceback (most recent call last):
763-
...
764-
mcpgateway.validation.jsonrpc.JSONRPCError: plugin error
782+
>>> result = asyncio.run(plugin_exception_handler(None, mock_error))
783+
>>> result.status_code
784+
200
785+
>>> content = json.loads(result.body.decode())
786+
>>> content["error"]["code"]
787+
-32603
788+
>>> "Plugin Error:" in content["error"]["message"]
789+
True
790+
>>> content["error"]["data"]["plugin_error_code"]
791+
'timeout'
792+
>>> content["error"]["data"]["plugin_name"]
793+
'abc'
765794
"""
766-
error_obj = exc.error.model_dump() if exc.error else {}
767795
message = exc.error.message if exc.error else "A plugin error occurred."
768796
status_code = exc.error.mcp_error_code if exc.error else -32603
769-
raise JSONRPCError(code=status_code, message=message, data=error_obj)
797+
error_details: dict[str, Any] = {}
798+
if exc.error:
799+
if exc.error.details:
800+
error_details["details"] = exc.error.details
801+
if exc.error.code:
802+
error_details["plugin_error_code"] = exc.error.code
803+
if exc.error.plugin_name:
804+
error_details["plugin_name"] = exc.error.plugin_name
805+
json_rpc_error = PydanticJSONRPCError(code=status_code, message="Plugin Error: " + message, data=error_details)
806+
return JSONResponse(status_code=200, content={"error": json_rpc_error.model_dump()})
770807

771808

772809
class DocsAuthMiddleware(BaseHTTPMiddleware):

tests/unit/mcpgateway/test_main.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,3 +1582,221 @@ def test_jsonpath_modifier_invalid_expressions(sample_people):
15821582

15831583
with pytest.raises(HTTPException):
15841584
jsonpath_modifier(sample_people, "$[*]", mappings={"bad": "$["}) # invalid mapping expr
1585+
1586+
1587+
# ----------------------------------------------------- #
1588+
# Plugin Exception Handler Tests #
1589+
# ----------------------------------------------------- #
1590+
class TestPluginExceptionHandlers:
1591+
"""Tests for plugin exception handlers: PluginViolationError and PluginError."""
1592+
1593+
def test_plugin_violation_exception_handler_with_full_violation(self):
1594+
"""Test plugin_violation_exception_handler with complete violation details."""
1595+
# Standard
1596+
import asyncio
1597+
1598+
# First-Party
1599+
from mcpgateway.main import plugin_violation_exception_handler
1600+
from mcpgateway.plugins.framework.errors import PluginViolationError
1601+
from mcpgateway.plugins.framework.models import PluginViolation
1602+
1603+
violation = PluginViolation(
1604+
reason="Invalid input",
1605+
description="The input contains prohibited content",
1606+
code="PROHIBITED_CONTENT",
1607+
details={"field": "message", "value": "sensitive_data"},
1608+
)
1609+
violation._plugin_name = "content_filter"
1610+
exc = PluginViolationError(message="Policy violation detected", violation=violation)
1611+
1612+
result = asyncio.run(plugin_violation_exception_handler(None, exc))
1613+
1614+
assert result.status_code == 200
1615+
content = json.loads(result.body.decode())
1616+
assert "error" in content
1617+
assert content["error"]["code"] == -32602
1618+
assert "Plugin Violation:" in content["error"]["message"]
1619+
assert "The input contains prohibited content" in content["error"]["message"]
1620+
assert content["error"]["data"]["description"] == "The input contains prohibited content"
1621+
assert content["error"]["data"]["details"] == {"field": "message", "value": "sensitive_data"}
1622+
assert content["error"]["data"]["plugin_error_code"] == "PROHIBITED_CONTENT"
1623+
assert content["error"]["data"]["plugin_name"] == "content_filter"
1624+
1625+
def test_plugin_violation_exception_handler_with_custom_mcp_error_code(self):
1626+
"""Test plugin_violation_exception_handler with custom MCP error code."""
1627+
# Standard
1628+
import asyncio
1629+
1630+
# First-Party
1631+
from mcpgateway.main import plugin_violation_exception_handler
1632+
from mcpgateway.plugins.framework.errors import PluginViolationError
1633+
from mcpgateway.plugins.framework.models import PluginViolation
1634+
1635+
violation = PluginViolation(
1636+
reason="Rate limit exceeded",
1637+
description="Too many requests from this client",
1638+
code="RATE_LIMIT",
1639+
details={"requests": 100, "limit": 50},
1640+
mcp_error_code=-32000, # Custom error code
1641+
)
1642+
violation._plugin_name = "rate_limiter"
1643+
exc = PluginViolationError(message="Rate limit violation", violation=violation)
1644+
1645+
result = asyncio.run(plugin_violation_exception_handler(None, exc))
1646+
1647+
assert result.status_code == 200
1648+
content = json.loads(result.body.decode())
1649+
assert content["error"]["code"] == -32000
1650+
assert "Too many requests from this client" in content["error"]["message"]
1651+
assert content["error"]["data"]["plugin_error_code"] == "RATE_LIMIT"
1652+
assert content["error"]["data"]["plugin_name"] == "rate_limiter"
1653+
1654+
def test_plugin_violation_exception_handler_with_minimal_violation(self):
1655+
"""Test plugin_violation_exception_handler with minimal violation details."""
1656+
# Standard
1657+
import asyncio
1658+
1659+
# First-Party
1660+
from mcpgateway.main import plugin_violation_exception_handler
1661+
from mcpgateway.plugins.framework.errors import PluginViolationError
1662+
from mcpgateway.plugins.framework.models import PluginViolation
1663+
1664+
violation = PluginViolation(
1665+
reason="Violation occurred",
1666+
description="Minimal violation",
1667+
code="MIN_VIOLATION",
1668+
details={},
1669+
)
1670+
exc = PluginViolationError(message="Minimal violation", violation=violation)
1671+
1672+
result = asyncio.run(plugin_violation_exception_handler(None, exc))
1673+
1674+
assert result.status_code == 200
1675+
content = json.loads(result.body.decode())
1676+
assert content["error"]["code"] == -32602
1677+
assert "Minimal violation" in content["error"]["message"]
1678+
assert content["error"]["data"]["plugin_error_code"] == "MIN_VIOLATION"
1679+
1680+
def test_plugin_violation_exception_handler_without_violation_object(self):
1681+
"""Test plugin_violation_exception_handler when violation object is None."""
1682+
# Standard
1683+
import asyncio
1684+
1685+
# First-Party
1686+
from mcpgateway.main import plugin_violation_exception_handler
1687+
from mcpgateway.plugins.framework.errors import PluginViolationError
1688+
1689+
exc = PluginViolationError(message="Generic plugin violation", violation=None)
1690+
1691+
result = asyncio.run(plugin_violation_exception_handler(None, exc))
1692+
1693+
assert result.status_code == 200
1694+
content = json.loads(result.body.decode())
1695+
assert content["error"]["code"] == -32602
1696+
assert "A plugin violation occurred" in content["error"]["message"]
1697+
assert content["error"]["data"] == {}
1698+
1699+
def test_plugin_exception_handler_with_full_error(self):
1700+
"""Test plugin_exception_handler with complete error details."""
1701+
# Standard
1702+
import asyncio
1703+
1704+
# First-Party
1705+
from mcpgateway.main import plugin_exception_handler
1706+
from mcpgateway.plugins.framework.errors import PluginError
1707+
from mcpgateway.plugins.framework.models import PluginErrorModel
1708+
1709+
error = PluginErrorModel(
1710+
message="Plugin execution failed",
1711+
code="EXECUTION_ERROR",
1712+
plugin_name="data_processor",
1713+
details={"error_type": "timeout", "duration": 30},
1714+
)
1715+
exc = PluginError(error=error)
1716+
1717+
result = asyncio.run(plugin_exception_handler(None, exc))
1718+
1719+
assert result.status_code == 200
1720+
content = json.loads(result.body.decode())
1721+
assert "error" in content
1722+
assert content["error"]["code"] == -32603
1723+
assert "Plugin Error:" in content["error"]["message"]
1724+
assert "Plugin execution failed" in content["error"]["message"]
1725+
assert content["error"]["data"]["details"] == {"error_type": "timeout", "duration": 30}
1726+
assert content["error"]["data"]["plugin_error_code"] == "EXECUTION_ERROR"
1727+
assert content["error"]["data"]["plugin_name"] == "data_processor"
1728+
1729+
def test_plugin_exception_handler_with_custom_mcp_error_code(self):
1730+
"""Test plugin_exception_handler with custom MCP error code."""
1731+
# Standard
1732+
import asyncio
1733+
1734+
# First-Party
1735+
from mcpgateway.main import plugin_exception_handler
1736+
from mcpgateway.plugins.framework.errors import PluginError
1737+
from mcpgateway.plugins.framework.models import PluginErrorModel
1738+
1739+
error = PluginErrorModel(
1740+
message="Custom error occurred",
1741+
code="CUSTOM_ERROR",
1742+
plugin_name="custom_plugin",
1743+
details={"context": "test"},
1744+
mcp_error_code=-32001, # Custom MCP error code
1745+
)
1746+
exc = PluginError(error=error)
1747+
1748+
result = asyncio.run(plugin_exception_handler(None, exc))
1749+
1750+
assert result.status_code == 200
1751+
content = json.loads(result.body.decode())
1752+
assert content["error"]["code"] == -32001
1753+
assert "Custom error occurred" in content["error"]["message"]
1754+
assert content["error"]["data"]["plugin_error_code"] == "CUSTOM_ERROR"
1755+
1756+
def test_plugin_exception_handler_with_minimal_error(self):
1757+
"""Test plugin_exception_handler with minimal error details."""
1758+
# Standard
1759+
import asyncio
1760+
1761+
# First-Party
1762+
from mcpgateway.main import plugin_exception_handler
1763+
from mcpgateway.plugins.framework.errors import PluginError
1764+
from mcpgateway.plugins.framework.models import PluginErrorModel
1765+
1766+
error = PluginErrorModel(message="Minimal error", plugin_name="minimal_plugin")
1767+
exc = PluginError(error=error)
1768+
1769+
result = asyncio.run(plugin_exception_handler(None, exc))
1770+
1771+
assert result.status_code == 200
1772+
content = json.loads(result.body.decode())
1773+
assert content["error"]["code"] == -32603
1774+
assert "Minimal error" in content["error"]["message"]
1775+
assert content["error"]["data"]["plugin_name"] == "minimal_plugin"
1776+
1777+
def test_plugin_exception_handler_with_empty_code(self):
1778+
"""Test plugin_exception_handler when error has empty code field."""
1779+
# Standard
1780+
import asyncio
1781+
1782+
# First-Party
1783+
from mcpgateway.main import plugin_exception_handler
1784+
from mcpgateway.plugins.framework.errors import PluginError
1785+
from mcpgateway.plugins.framework.models import PluginErrorModel
1786+
1787+
error = PluginErrorModel(
1788+
message="Error without code",
1789+
code="",
1790+
plugin_name="test_plugin",
1791+
details={"info": "test"},
1792+
)
1793+
exc = PluginError(error=error)
1794+
1795+
result = asyncio.run(plugin_exception_handler(None, exc))
1796+
1797+
assert result.status_code == 200
1798+
content = json.loads(result.body.decode())
1799+
assert content["error"]["code"] == -32603
1800+
assert "Error without code" in content["error"]["message"]
1801+
# Empty code should not be included in data
1802+
assert "plugin_error_code" not in content["error"]["data"] or content["error"]["data"]["plugin_error_code"] == ""

0 commit comments

Comments
 (0)