Skip to content

Commit ca6d25a

Browse files
feat: provide more informative validate error messages for tool calls
- Enhanced error handling in Processor to provide more informative validation error messages. - Changed server listening address from '0.0.0.0' to '127.0.0.1' in example server scripts for improved clarity and compatibility. - Refined logging messages in HttpServerTransport for consistency and clarity.
1 parent bee773e commit ca6d25a

File tree

5 files changed

+51
-23
lines changed

5 files changed

+51
-23
lines changed

examples/04-combined-registration-http/server.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
|
2020
| To Use:
2121
| 1. Run this script from your CLI: `php server.php`
22-
| The server will listen on http://0.0.0.0:8081 by default.
22+
| The server will listen on http://127.0.0.1:8081 by default.
2323
| 2. Configure your MCP Client (e.g., Cursor):
2424
|
2525
| {
@@ -80,7 +80,7 @@ public function log($level, \Stringable|string $message, array $context = []): v
8080
// If 'config://priority' was discovered, the manual one takes precedence.
8181
$server->discover(__DIR__, scanDirs: ['.']);
8282

83-
$transport = new HttpServerTransport('0.0.0.0', 8081, 'mcp_combined');
83+
$transport = new HttpServerTransport('127.0.0.1', 8081, 'mcp_combined');
8484

8585
$server->listen($transport);
8686

examples/07-complex-tool-schema-http/server.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function log($level, \Stringable|string $message, array $context = []): v
8282

8383
$server->discover(__DIR__, ['.']);
8484

85-
$transport = new HttpServerTransport('120.0.0.1', 8082, 'mcp_scheduler');
85+
$transport = new HttpServerTransport('127.0.0.1', 8082, 'mcp_scheduler');
8686
$server->listen($transport);
8787

8888
$logger->info('Server listener stopped gracefully.');

src/Processor.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,18 +299,33 @@ private function handleToolCall(array $params): CallToolResult
299299

300300
$definition = $this->registry->findTool($toolName);
301301
if (! $definition) {
302-
throw McpServerException::methodNotFound("Tool '{$toolName}' not found."); // Method not found seems appropriate
302+
throw McpServerException::methodNotFound("Tool '{$toolName}' not found.");
303303
}
304304

305305
$inputSchema = $definition->getInputSchema();
306306
$argumentsForValidation = is_object($argumentsRaw) ? (array) $argumentsRaw : $argumentsRaw;
307307

308308
$validationErrors = $this->schemaValidator->validateAgainstJsonSchema($argumentsForValidation, $inputSchema);
309+
309310
if (! empty($validationErrors)) {
310-
throw McpServerException::invalidParams(data: ['validation_errors' => $validationErrors]);
311+
$errorMessages = [];
312+
313+
foreach ($validationErrors as $errorDetail) {
314+
$pointer = $errorDetail['pointer'] ?? '';
315+
$message = $errorDetail['message'] ?? 'Unknown validation error';
316+
$errorMessages[] = ($pointer !== '/' && $pointer !== '' ? "Property '{$pointer}': " : '').$message;
317+
}
318+
319+
$summaryMessage = "Invalid parameters for tool '{$toolName}': ".implode('; ', array_slice($errorMessages, 0, 3));
320+
321+
if (count($errorMessages) > 3) {
322+
$summaryMessage .= '; ...and more errors.';
323+
}
324+
325+
throw McpServerException::invalidParams($summaryMessage, data: ['validation_errors' => $validationErrors]);
311326
}
312327

313-
$argumentsForPhpCall = (array) $argumentsRaw; // Need array for ArgumentPreparer
328+
$argumentsForPhpCall = (array) $argumentsRaw;
314329

315330
try {
316331
$instance = $this->container->get($definition->getClassName());

src/Support/SchemaValidator.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22

33
namespace PhpMcp\Server\Support;
44

5+
use InvalidArgumentException;
56
use JsonException;
67
use Opis\JsonSchema\Errors\ValidationError;
78
use Opis\JsonSchema\Validator;
89
use Psr\Log\LoggerInterface;
910
use Throwable;
10-
use InvalidArgumentException;
1111

1212
/**
1313
* Validates data against JSON Schema definitions using opis/json-schema.
1414
*/
1515
class SchemaValidator
1616
{
1717
private ?Validator $jsonSchemaValidator = null;
18+
1819
private LoggerInterface $logger;
1920

2021
public function __construct(LoggerInterface $logger)
@@ -50,12 +51,15 @@ public function validateAgainstJsonSchema(mixed $data, array|object $schema): ar
5051

5152
} catch (JsonException $e) {
5253
$this->logger->error('MCP SDK: Invalid schema structure provided for validation (JSON conversion failed).', ['exception' => $e]);
54+
5355
return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Invalid schema definition provided (JSON error).']];
5456
} catch (InvalidArgumentException $e) {
5557
$this->logger->error('MCP SDK: Invalid schema structure provided for validation.', ['exception' => $e]);
58+
5659
return [['pointer' => '', 'keyword' => 'internal', 'message' => $e->getMessage()]];
5760
} catch (Throwable $e) {
5861
$this->logger->error('MCP SDK: Error preparing data/schema for validation.', ['exception' => $e]);
62+
5963
return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Internal validation preparation error.']];
6064
}
6165

@@ -70,6 +74,7 @@ public function validateAgainstJsonSchema(mixed $data, array|object $schema): ar
7074
'data' => json_encode($dataToValidate),
7175
'schema' => json_encode($schemaObject),
7276
]);
77+
7378
return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Schema validation process failed: '.$e->getMessage()]];
7479
}
7580

@@ -101,9 +106,10 @@ public function validateAgainstJsonSchema(mixed $data, array|object $schema): ar
101106
private function getJsonSchemaValidator(): Validator
102107
{
103108
if ($this->jsonSchemaValidator === null) {
104-
$this->jsonSchemaValidator = new Validator();
109+
$this->jsonSchemaValidator = new Validator;
105110
// Potentially configure resolver here if needed later
106111
}
112+
107113
return $this->jsonSchemaValidator;
108114
}
109115

@@ -114,24 +120,27 @@ private function convertDataForValidator(mixed $data): mixed
114120
{
115121
if (is_array($data)) {
116122
// Check if it's an associative array (keys are not sequential numbers 0..N-1)
117-
if (!empty($data) && array_keys($data) !== range(0, count($data) - 1)) {
118-
$obj = new \stdClass();
123+
if (! empty($data) && array_keys($data) !== range(0, count($data) - 1)) {
124+
$obj = new \stdClass;
119125
foreach ($data as $key => $value) {
120126
$obj->{$key} = $this->convertDataForValidator($value);
121127
}
128+
122129
return $obj;
123130
} else {
124131
// It's a list (sequential array), convert items recursively
125132
return array_map([$this, 'convertDataForValidator'], $data);
126133
}
127134
} elseif (is_object($data) && $data instanceof \stdClass) {
128135
// Deep copy/convert stdClass objects as well
129-
$obj = new \stdClass();
136+
$obj = new \stdClass;
130137
foreach (get_object_vars($data) as $key => $value) {
131138
$obj->{$key} = $this->convertDataForValidator($value);
132139
}
140+
133141
return $obj;
134142
}
143+
135144
// Leave other objects and scalar types as they are
136145
return $data;
137146
}
@@ -165,8 +174,10 @@ private function formatJsonPointerPath(?array $pathComponents): string
165174
}
166175
$escapedComponents = array_map(function ($component) {
167176
$componentStr = (string) $component;
177+
168178
return str_replace(['~', '/'], ['~0', '~1'], $componentStr);
169179
}, $pathComponents);
180+
170181
return '/'.implode('/', $escapedComponents);
171182
}
172183

@@ -213,6 +224,7 @@ private function formatValidationError(ValidationError $error): string
213224
if ($v === null) {
214225
return 'null';
215226
}
227+
216228
return (string) $v;
217229
}, $allowedValues);
218230
$message = 'Value must be one of the allowed values: '.implode(', ', $formattedAllowed).'.';
@@ -289,12 +301,14 @@ private function formatValidationError(ValidationError $error): string
289301
$builtInMessage = preg_replace_callback('/\{(\w+)\}/', function ($match) use ($placeholders) {
290302
$key = $match[1];
291303
$value = $placeholders[$key] ?? '{'.$key.'}';
304+
292305
return is_array($value) ? json_encode($value) : (string) $value;
293306
}, $builtInMessage);
294307
$message = $builtInMessage;
295308
}
296309
break;
297310
}
311+
298312
return $message;
299313
}
300314
}

src/Transports/HttpServerTransport.php

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function __construct(
6565
private readonly string $mcpPathPrefix = 'mcp', // e.g., /mcp/sse, /mcp/message
6666
private readonly ?array $sslContext = null // For enabling HTTPS
6767
) {
68-
$this->logger = new NullLogger();
68+
$this->logger = new NullLogger;
6969
$this->loop = Loop::get();
7070
$this->ssePath = '/'.trim($mcpPathPrefix, '/').'/sse';
7171
$this->messagePath = '/'.trim($mcpPathPrefix, '/').'/message';
@@ -109,7 +109,7 @@ public function listen(): void
109109
$this->http->listen($this->socket);
110110

111111
$this->socket->on('error', function (Throwable $error) {
112-
$this->logger->error('HttpTransport: Socket server error.', ['error' => $error->getMessage()]);
112+
$this->logger->error('Socket server error.', ['error' => $error->getMessage()]);
113113
$this->emit('error', [new TransportException("Socket server error: {$error->getMessage()}", 0, $error)]);
114114
$this->close();
115115
});
@@ -159,7 +159,7 @@ private function handleSseRequest(ServerRequestInterface $request): Response
159159
$clientId = 'sse_'.bin2hex(random_bytes(16));
160160
$this->logger->info('New SSE connection', ['clientId' => $clientId]);
161161

162-
$sseStream = new ThroughStream();
162+
$sseStream = new ThroughStream;
163163

164164
$sseStream->on('close', function () use ($clientId) {
165165
$this->logger->info('SSE stream closed', ['clientId' => $clientId]);
@@ -216,13 +216,13 @@ private function handleMessagePostRequest(ServerRequestInterface $request): Resp
216216
$clientId = $queryParams['clientId'] ?? null;
217217

218218
if (! $clientId || ! is_string($clientId)) {
219-
$this->logger->warning('HttpTransport: Received POST without valid clientId query parameter.');
219+
$this->logger->warning('Received POST without valid clientId query parameter.');
220220

221221
return new Response(400, ['Content-Type' => 'text/plain'], 'Missing or invalid clientId query parameter');
222222
}
223223

224224
if (! isset($this->activeSseStreams[$clientId])) {
225-
$this->logger->warning('HttpTransport: Received POST for unknown or disconnected clientId.', ['clientId' => $clientId]);
225+
$this->logger->warning('Received POST for unknown or disconnected clientId.', ['clientId' => $clientId]);
226226

227227
return new Response(404, ['Content-Type' => 'text/plain'], 'Client ID not found or disconnected');
228228
}
@@ -234,7 +234,7 @@ private function handleMessagePostRequest(ServerRequestInterface $request): Resp
234234
$body = $request->getBody()->getContents();
235235

236236
if (empty($body)) {
237-
$this->logger->warning('HttpTransport: Received empty POST body', ['clientId' => $clientId]);
237+
$this->logger->warning('Received empty POST body', ['clientId' => $clientId]);
238238

239239
return new Response(400, ['Content-Type' => 'text/plain'], 'Empty request body');
240240
}
@@ -265,16 +265,15 @@ public function sendToClientAsync(string $clientId, string $rawFramedMessage): P
265265
return \React\Promise\resolve(null);
266266
}
267267

268-
$deferred = new Deferred();
268+
$deferred = new Deferred;
269269
$written = $this->sendSseEvent($stream, 'message', $jsonData);
270270

271271
if ($written) {
272-
$this->logger->debug('HttpTransport: Message sent via SSE.', ['clientId' => $clientId, 'data' => $jsonData]);
273272
$deferred->resolve(null);
274273
} else {
275-
$this->logger->debug('HttpTransport: SSE stream buffer full, waiting for drain.', ['clientId' => $clientId]);
274+
$this->logger->debug('SSE stream buffer full, waiting for drain.', ['clientId' => $clientId]);
276275
$stream->once('drain', function () use ($deferred, $clientId) {
277-
$this->logger->debug('HttpTransport: SSE stream drained.', ['clientId' => $clientId]);
276+
$this->logger->debug('SSE stream drained.', ['clientId' => $clientId]);
278277
$deferred->resolve(null);
279278
});
280279
// Add a timeout?
@@ -316,7 +315,7 @@ public function close(): void
316315
}
317316
$this->closing = true;
318317
$this->listening = false;
319-
$this->logger->info('HttpTransport: Closing...');
318+
$this->logger->info('Closing transport...');
320319

321320
if ($this->socket) {
322321
$this->socket->close();
@@ -326,7 +325,7 @@ public function close(): void
326325
$activeStreams = $this->activeSseStreams;
327326
$this->activeSseStreams = [];
328327
foreach ($activeStreams as $clientId => $stream) {
329-
$this->logger->debug('HttpTransport: Closing active SSE stream', ['clientId' => $clientId]);
328+
$this->logger->debug('Closing active SSE stream', ['clientId' => $clientId]);
330329
unset($this->activeSseStreams[$clientId]);
331330
$stream->close();
332331
}

0 commit comments

Comments
 (0)