Skip to content

Commit 32b8b2e

Browse files
[Server] Fix: ensure JSON-RPC error responses include message IDs (#77)
* fix: ensure JSON-RPC error responses include message IDs * fix: replace method_exists with instanceof for message ID extraction
1 parent 325b4aa commit 32b8b2e

File tree

3 files changed

+38
-35
lines changed

3 files changed

+38
-35
lines changed

src/JsonRpc/Handler.php

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Mcp\Schema\Implementation;
2424
use Mcp\Schema\JsonRpc\Error;
2525
use Mcp\Schema\JsonRpc\HasMethodInterface;
26+
use Mcp\Schema\JsonRpc\Request;
2627
use Mcp\Schema\JsonRpc\Response;
2728
use Mcp\Schema\Request\InitializeRequest;
2829
use Mcp\Server\MethodHandlerInterface;
@@ -94,9 +95,6 @@ public static function make(
9495

9596
/**
9697
* @return iterable<array{string|null, array<string, mixed>}>
97-
*
98-
* @throws ExceptionInterface When a handler throws an exception during message processing
99-
* @throws \JsonException When JSON encoding of the response fails
10098
*/
10199
public function process(string $input, ?Uuid $sessionId): iterable
102100
{
@@ -163,7 +161,7 @@ public function process(string $input, ?Uuid $sessionId): iterable
163161
foreach ($messages as $message) {
164162
if ($message instanceof InvalidInputMessageException) {
165163
$this->logger->warning('Failed to create message.', ['exception' => $message]);
166-
$error = Error::forInvalidRequest($message->getMessage(), 0);
164+
$error = Error::forInvalidRequest($message->getMessage());
167165
yield [$this->encodeResponse($error), []];
168166
continue;
169167
}
@@ -172,6 +170,8 @@ public function process(string $input, ?Uuid $sessionId): iterable
172170
'method' => $message->getMethod(),
173171
]);
174172

173+
$messageId = $message instanceof Request ? $message->getId() : 0;
174+
175175
try {
176176
$response = $this->handle($message, $session);
177177
yield [$this->encodeResponse($response), ['session_id' => $session->getId()]];
@@ -183,17 +183,17 @@ public function process(string $input, ?Uuid $sessionId): iterable
183183
['exception' => $e],
184184
);
185185

186-
$error = Error::forMethodNotFound($e->getMessage());
186+
$error = Error::forMethodNotFound($e->getMessage(), $messageId);
187187
yield [$this->encodeResponse($error), []];
188188
} catch (\InvalidArgumentException $e) {
189189
$this->logger->warning(\sprintf('Invalid argument: %s', $e->getMessage()), ['exception' => $e]);
190190

191-
$error = Error::forInvalidParams($e->getMessage());
191+
$error = Error::forInvalidParams($e->getMessage(), $messageId);
192192
yield [$this->encodeResponse($error), []];
193193
} catch (\Throwable $e) {
194194
$this->logger->critical(\sprintf('Uncaught exception: %s', $e->getMessage()), ['exception' => $e]);
195195

196-
$error = Error::forInternalError($e->getMessage());
196+
$error = Error::forInternalError($e->getMessage(), $messageId);
197197
yield [$this->encodeResponse($error), []];
198198
}
199199
}
@@ -202,7 +202,7 @@ public function process(string $input, ?Uuid $sessionId): iterable
202202
}
203203

204204
/**
205-
* @throws \JsonException When JSON encoding fails
205+
* Encodes a response to JSON, handling encoding errors gracefully.
206206
*/
207207
private function encodeResponse(Response|Error|null $response): ?string
208208
{
@@ -214,11 +214,26 @@ private function encodeResponse(Response|Error|null $response): ?string
214214

215215
$this->logger->info('Encoding response.', ['response' => $response]);
216216

217-
if ($response instanceof Response && [] === $response->result) {
218-
return json_encode($response, \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT);
219-
}
217+
try {
218+
if ($response instanceof Response && [] === $response->result) {
219+
return json_encode($response, \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT);
220+
}
220221

221-
return json_encode($response, \JSON_THROW_ON_ERROR);
222+
return json_encode($response, \JSON_THROW_ON_ERROR);
223+
} catch (\JsonException $e) {
224+
$this->logger->error('Failed to encode response to JSON.', [
225+
'message_id' => $response->getId(),
226+
'exception' => $e,
227+
]);
228+
229+
$fallbackError = new Error(
230+
id: $response->getId(),
231+
code: Error::INTERNAL_ERROR,
232+
message: 'Response could not be encoded to JSON'
233+
);
234+
235+
return json_encode($fallbackError, \JSON_THROW_ON_ERROR);
236+
}
222237
}
223238

224239
/**

src/Server.php

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,12 @@ public function connect(TransportInterface $transport): void
4444
]);
4545

4646
$transport->onMessage(function (string $message, ?Uuid $sessionId) use ($transport) {
47-
try {
48-
foreach ($this->jsonRpcHandler->process($message, $sessionId) as [$response, $context]) {
49-
if (null === $response) {
50-
continue;
51-
}
52-
53-
$transport->send($response, $context);
47+
foreach ($this->jsonRpcHandler->process($message, $sessionId) as [$response, $context]) {
48+
if (null === $response) {
49+
continue;
5450
}
55-
} catch (\JsonException $e) {
56-
$this->logger->error('Failed to encode response to JSON.', [
57-
'message' => $message,
58-
'exception' => $e,
59-
]);
51+
52+
$transport->send($response, $context);
6053
}
6154
});
6255

tests/ServerTest.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,32 @@
1414
use Mcp\JsonRpc\Handler;
1515
use Mcp\Server;
1616
use Mcp\Server\Transport\InMemoryTransport;
17-
use PHPUnit\Framework\MockObject\Stub\Exception;
1817
use PHPUnit\Framework\TestCase;
19-
use Psr\Log\NullLogger;
2018

2119
class ServerTest extends TestCase
2220
{
2321
public function testJsonExceptions()
2422
{
25-
$logger = $this->getMockBuilder(NullLogger::class)
26-
->disableOriginalConstructor()
27-
->onlyMethods(['error'])
28-
->getMock();
29-
$logger->expects($this->once())->method('error');
30-
3123
$handler = $this->getMockBuilder(Handler::class)
3224
->disableOriginalConstructor()
3325
->onlyMethods(['process'])
3426
->getMock();
3527

3628
$handler->expects($this->exactly(2))->method('process')->willReturnOnConsecutiveCalls(
37-
new Exception(new \JsonException('foobar')),
29+
[['{"jsonrpc":"2.0","id":0,"error":{"code":-32700,"message":"Parse error"}}', []]],
3830
[['success', []]]
3931
);
4032

4133
$transport = $this->getMockBuilder(InMemoryTransport::class)
4234
->setConstructorArgs([['foo', 'bar']])
4335
->onlyMethods(['send'])
4436
->getMock();
45-
$transport->expects($this->once())->method('send')->with('success', []);
37+
$transport->expects($this->exactly(2))->method('send')->willReturnOnConsecutiveCalls(
38+
null,
39+
null
40+
);
4641

47-
$server = new Server($handler, $logger);
42+
$server = new Server($handler);
4843
$server->connect($transport);
4944

5045
$transport->listen();

0 commit comments

Comments
 (0)