diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f80ef9..a5ee3670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions. * Fix OIDC discovery rejecting issuers with a trailing slash (e.g. Authentik, Auth0). * Fix stateless SSE streams holding back frames until close when PHP output buffering is enabled. +* Log the tool name of a `tools/call` at info level in `CallToolHandler`, so a server logging at INFO shows which tool is called; its arguments stay at debug level. * Reject a recognized `Mcp-Param-*` header whose mirrored argument is absent from the body with `-32020`, instead of accepting the request (SEP-2243). * Fix `JwtTokenValidator` with several issuers always fetching the keys of the first one: keys now come from the issuer the token claims, which must be configured. * Fix `RequestEvent`, `ResponseEvent` and `ErrorEvent` not being dispatched for `2026-07-28` requests. diff --git a/src/Server/Handler/Request/CallToolHandler.php b/src/Server/Handler/Request/CallToolHandler.php index dc81a672..2a4a7dd9 100644 --- a/src/Server/Handler/Request/CallToolHandler.php +++ b/src/Server/Handler/Request/CallToolHandler.php @@ -67,6 +67,8 @@ public function handle(Request $request, SessionInterface $session): Response|Er $toolName = $request->name; $arguments = $request->arguments; + // The arguments may carry sensitive input, so only the name is logged at info level. + $this->logger->info('Calling tool', ['name' => $toolName]); $this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]); try { diff --git a/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php b/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php index 572c1b0f..80de8f09 100644 --- a/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php +++ b/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php @@ -288,6 +288,45 @@ public function log($level, $message, array $context = []): void ))); } + public function testToolNameIsLoggedAtInfoLevelAndArgumentsOnlyAtDebugLevel(): void + { + $request = $this->createCallToolRequest('login', ['password' => 's3cr3t-argument']); + $logger = new class extends AbstractLogger { + public array $records = []; + + // @phpstan-ignore missingType.parameter (compatible with psr/log 1.x) + public function log($level, $message, array $context = []): void + { + $this->records[] = ['level' => $level, 'message' => (string) $message, 'context' => $context]; + } + }; + $handler = new CallToolHandler($this->registry, $this->referenceHandler, $logger); + $toolReference = $this->createToolReference('login', static fn () => 'ok'); + + $this->registry->method('getTool')->willReturn($toolReference); + $this->referenceHandler->method('handle')->willReturn('ok'); + $toolReference->method('formatResult')->willReturn([new TextContent('ok')]); + + $handler->handle($request, $this->session); + + $infoRecords = array_values(array_filter( + $logger->records, + static fn (array $record): bool => 'info' === $record['level'], + )); + $this->assertSame([ + ['level' => 'info', 'message' => 'Calling tool', 'context' => ['name' => 'login']], + ], $infoRecords); + + $recordsWithArguments = array_values(array_filter( + $logger->records, + static fn (array $record): bool => \array_key_exists('arguments', $record['context']), + )); + $this->assertNotSame([], $recordsWithArguments); + foreach ($recordsWithArguments as $record) { + $this->assertSame('debug', $record['level']); + } + } + public function testHandleWithNullResult(): void { $request = $this->createCallToolRequest('null_tool', []);