Description
A RequestEvent listener can change the request with setRequest(), but it has no way to reject it. ErrorEvent already has setError(). RequestEvent has nothing like it.
We ran into this in Drupal's mcp_server_oauth module. It checks per-tool OAuth scopes in a RequestEvent listener. To refuse a call, the listener throws, and until 0.8 that exception escaped processInput() so Drupal could turn it into a 401 or 403. That was never a documented contract. It stopped working when #412 added a guard around each message. The guard is correct, and we're not asking to remove it. Now every thrown refusal becomes -32603:
{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"Internal server error."}}
It's also logged as "Uncaught exception while handling message".
Proposal
Let a listener set an Error on RequestEvent. If one is set, handleRequest() sends it and skips the handler:
$event = $this->dispatchEvent(new RequestEvent($request, $session));
if (null !== $error = $event->getError()) {
$this->sendResponse($transport, $error, $session);
return;
}
$request = $event->getRequest();
This covers checks that need the parsed request, such as per-tool policy, where AuthorizationMiddleware can't help because it only sees the token.
Resource reads have a related gap. ReadResourceHandler turns any exception into Error while reading resource. It already rethrows MissingRequiredClientCapabilityException, so a documented exception type that's rethrown the same way would let a resource refuse access with a proper error.
Open question
A JSON-RPC error doesn't give the HTTP transport a status code. For per-tool scopes, the MCP authorization spec expects a 403 with WWW-Authenticate: Bearer error="insufficient_scope". Is that in scope for the SDK, or should hosts check scopes in HTTP middleware before the message reaches Protocol?
Description
A
RequestEventlistener can change the request withsetRequest(), but it has no way to reject it.ErrorEventalready hassetError().RequestEventhas nothing like it.We ran into this in Drupal's
mcp_server_oauthmodule. It checks per-tool OAuth scopes in aRequestEventlistener. To refuse a call, the listener throws, and until 0.8 that exception escapedprocessInput()so Drupal could turn it into a 401 or 403. That was never a documented contract. It stopped working when #412 added a guard around each message. The guard is correct, and we're not asking to remove it. Now every thrown refusal becomes-32603:{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"Internal server error."}}It's also logged as "Uncaught exception while handling message".
Proposal
Let a listener set an
ErroronRequestEvent. If one is set,handleRequest()sends it and skips the handler:This covers checks that need the parsed request, such as per-tool policy, where
AuthorizationMiddlewarecan't help because it only sees the token.Resource reads have a related gap.
ReadResourceHandlerturns any exception intoError while reading resource. It already rethrowsMissingRequiredClientCapabilityException, so a documented exception type that's rethrown the same way would let a resource refuse access with a proper error.Open question
A JSON-RPC error doesn't give the HTTP transport a status code. For per-tool scopes, the MCP authorization spec expects a 403 with
WWW-Authenticate: Bearer error="insufficient_scope". Is that in scope for the SDK, or should hosts check scopes in HTTP middleware before the message reachesProtocol?