Skip to content

Commit ef08de4

Browse files
committed
Fix Issue
1 parent b5b2551 commit ef08de4

File tree

6 files changed

+257
-33
lines changed

6 files changed

+257
-33
lines changed

src/Server.php

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ abstract class Server
6666
'prompts' => [
6767
'listChanged' => false,
6868
],
69-
'logging' => [],
7069
];
7170

7271
/**
@@ -233,19 +232,23 @@ public function createContext(): ServerContext
233232
*/
234233
protected function handleMessage(JsonRpcRequest $request, ServerContext $context): void
235234
{
236-
$response = $this->runMethodHandle($request, $context);
237-
238-
if (! is_iterable($response)) {
239-
$this->transport->send($response->toJson());
235+
try {
236+
$response = $this->runMethodHandle($request, $context);
240237

241-
return;
242-
}
238+
if (! is_iterable($response)) {
239+
$this->transport->send($response->toJson());
243240

244-
$this->transport->stream(function () use ($response): void {
245-
foreach ($response as $message) {
246-
$this->transport->send($message->toJson());
241+
return;
247242
}
248-
});
243+
244+
$this->transport->stream(function () use ($response): void {
245+
foreach ($response as $message) {
246+
$this->transport->send($message->toJson());
247+
}
248+
});
249+
} finally {
250+
Container::getInstance()->forgetInstance('mcp.request');
251+
}
249252
}
250253

251254
/**
@@ -257,20 +260,14 @@ protected function runMethodHandle(JsonRpcRequest $request, ServerContext $conte
257260
{
258261
$container = Container::getInstance();
259262

263+
$container->instance('mcp.request', $request->toRequest());
264+
260265
/** @var Method $methodClass */
261266
$methodClass = $container->make(
262267
$this->methods[$request->method],
263268
);
264269

265-
$container->instance('mcp.request', $request->toRequest());
266-
267-
try {
268-
$response = $methodClass->handle($request, $context);
269-
} finally {
270-
$container->forgetInstance('mcp.request');
271-
}
272-
273-
return $response;
270+
return $methodClass->handle($request, $context);
274271
}
275272

276273
protected function handleInitializeMessage(JsonRpcRequest $request, ServerContext $context): void

src/Server/McpServiceProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ protected function registerSessionBindings(): void
108108
});
109109

110110
$this->app->bind(LoggingManager::class, fn ($app): LoggingManager => new LoggingManager(
111-
$app->make(Support\SessionStoreManager::class)
111+
$app->make(SessionStoreManager::class)
112112
));
113113
}
114114

src/Server/Methods/Concerns/InteractsWithResponses.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,20 @@ protected function toJsonRpcStreamedResponse(JsonRpcRequest $request, iterable $
4747
{
4848
/** @var array<int, Response|ResponseFactory|string> $pendingResponses */
4949
$pendingResponses = [];
50-
$loggingManager = app(LoggingManager::class);
50+
$loggingManager = null;
5151

5252
try {
5353
foreach ($responses as $response) {
5454
if ($response instanceof Response && $response->isNotification()) {
5555
/** @var Notification $content */
5656
$content = $response->content();
5757

58-
if ($content instanceof LogNotification && ! $loggingManager->shouldLog($content->level())) {
59-
continue;
58+
if ($content instanceof LogNotification) {
59+
$loggingManager ??= app(LoggingManager::class);
60+
61+
if (! $loggingManager->shouldLog($content->level())) {
62+
continue;
63+
}
6064
}
6165

6266
yield JsonRpcResponse::notification(
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Laravel\Mcp\Server\Support\LoggingManager;
6+
use Laravel\Mcp\Server\Support\SessionStoreManager;
7+
8+
it('resolves LoggingManager from container without producing output', function (): void {
9+
// Capture any output that might be produced during resolution
10+
ob_start();
11+
12+
// Capture any errors/warnings
13+
$errors = [];
14+
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use (&$errors): bool {
15+
$errors[] = [
16+
'type' => $errno,
17+
'message' => $errstr,
18+
'file' => $errfile,
19+
'line' => $errline,
20+
];
21+
22+
return true; // Don't execute PHP's internal error handler
23+
});
24+
25+
try {
26+
// First, bind LoggingManager in the container (simulating what we want to test)
27+
app()->bind(LoggingManager::class, fn ($app): LoggingManager => new LoggingManager(
28+
$app->make(SessionStoreManager::class)
29+
));
30+
31+
// Now resolve it
32+
$manager = app(LoggingManager::class);
33+
34+
expect($manager)->toBeInstanceOf(LoggingManager::class);
35+
} finally {
36+
restore_error_handler();
37+
}
38+
39+
$output = ob_get_clean();
40+
41+
// If there was any output, fail with diagnostic info
42+
if ($output !== '' && $output !== false) {
43+
$hexDump = bin2hex(substr($output, 0, 200));
44+
throw new \RuntimeException(sprintf(
45+
"Unexpected output during LoggingManager resolution!\nLength: %d bytes\nContent: %s\nHex: %s",
46+
strlen($output),
47+
substr($output, 0, 500),
48+
$hexDump
49+
));
50+
}
51+
52+
// If there were any errors/warnings, fail with diagnostic info
53+
if ($errors !== []) {
54+
throw new \RuntimeException(sprintf(
55+
"Errors/warnings during LoggingManager resolution:\n%s",
56+
json_encode($errors, JSON_PRETTY_PRINT)
57+
));
58+
}
59+
60+
expect($output)->toBe('');
61+
expect($errors)->toBe([]);
62+
});
63+
64+
it('resolves SessionStoreManager from container without producing output', function (): void {
65+
ob_start();
66+
67+
$errors = [];
68+
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use (&$errors): bool {
69+
$errors[] = [
70+
'type' => $errno,
71+
'message' => $errstr,
72+
'file' => $errfile,
73+
'line' => $errline,
74+
];
75+
76+
return true;
77+
});
78+
79+
try {
80+
$manager = app(SessionStoreManager::class);
81+
expect($manager)->toBeInstanceOf(SessionStoreManager::class);
82+
} finally {
83+
restore_error_handler();
84+
}
85+
86+
$output = ob_get_clean();
87+
88+
if ($output !== '' && $output !== false) {
89+
throw new \RuntimeException(sprintf(
90+
"Unexpected output during SessionStoreManager resolution!\nLength: %d bytes\nContent: %s",
91+
strlen($output),
92+
substr($output, 0, 500)
93+
));
94+
}
95+
96+
if ($errors !== []) {
97+
throw new \RuntimeException(sprintf(
98+
"Errors/warnings during SessionStoreManager resolution:\n%s",
99+
json_encode($errors, JSON_PRETTY_PRINT)
100+
));
101+
}
102+
103+
expect($output)->toBe('');
104+
});
105+
106+
it('resolves Cache repository without producing output', function (): void {
107+
ob_start();
108+
109+
$errors = [];
110+
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use (&$errors): bool {
111+
$errors[] = [
112+
'type' => $errno,
113+
'message' => $errstr,
114+
'file' => $errfile,
115+
'line' => $errline,
116+
];
117+
118+
return true;
119+
});
120+
121+
try {
122+
$cache = app(\Illuminate\Contracts\Cache\Repository::class);
123+
expect($cache)->toBeInstanceOf(\Illuminate\Contracts\Cache\Repository::class);
124+
} finally {
125+
restore_error_handler();
126+
}
127+
128+
$output = ob_get_clean();
129+
130+
if ($output !== '' && $output !== false) {
131+
throw new \RuntimeException(sprintf(
132+
"Unexpected output during Cache resolution!\nLength: %d bytes\nContent: %s",
133+
strlen($output),
134+
substr($output, 0, 500)
135+
));
136+
}
137+
138+
if ($errors !== []) {
139+
throw new \RuntimeException(sprintf(
140+
"Errors/warnings during Cache resolution:\n%s",
141+
json_encode($errors, JSON_PRETTY_PRINT)
142+
));
143+
}
144+
145+
expect($output)->toBe('');
146+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Support\Facades\Cache;
6+
use Laravel\Mcp\Enums\LogLevel;
7+
use Laravel\Mcp\Server\Support\LoggingManager;
8+
use Laravel\Mcp\Server\Support\SessionStoreManager;
9+
use Tests\Fixtures\ArrayTransport;
10+
use Tests\Fixtures\ExampleServer;
11+
12+
it('persists log level to cache through server request flow', function (): void {
13+
$transport = new ArrayTransport;
14+
$server = new ExampleServer($transport);
15+
16+
$server->start();
17+
18+
$sessionId = 'test-session-'.uniqid();
19+
$transport->sessionId = $sessionId;
20+
21+
$payload = json_encode([
22+
'jsonrpc' => '2.0',
23+
'id' => 1,
24+
'method' => 'logging/setLevel',
25+
'params' => [
26+
'level' => 'error',
27+
],
28+
]);
29+
30+
($transport->handler)($payload);
31+
32+
$response = json_decode((string) $transport->sent[0], true);
33+
34+
expect($response)->toHaveKey('result')
35+
->and($response['id'])->toBe(1);
36+
37+
$manager = new LoggingManager(new SessionStoreManager(Cache::driver(), $sessionId));
38+
expect($manager->getLevel())->toBe(LogLevel::Error);
39+
});
40+
41+
it('correctly isolates log levels per session', function (): void {
42+
$transport1 = new ArrayTransport;
43+
$server1 = new ExampleServer($transport1);
44+
$server1->start();
45+
46+
$transport2 = new ArrayTransport;
47+
$server2 = new ExampleServer($transport2);
48+
$server2->start();
49+
50+
$sessionId1 = 'session-1-'.uniqid();
51+
$sessionId2 = 'session-2-'.uniqid();
52+
53+
$transport1->sessionId = $sessionId1;
54+
($transport1->handler)(json_encode([
55+
'jsonrpc' => '2.0',
56+
'id' => 1,
57+
'method' => 'logging/setLevel',
58+
'params' => ['level' => 'debug'],
59+
]));
60+
61+
$transport2->sessionId = $sessionId2;
62+
($transport2->handler)(json_encode([
63+
'jsonrpc' => '2.0',
64+
'id' => 2,
65+
'method' => 'logging/setLevel',
66+
'params' => ['level' => 'error'],
67+
]));
68+
69+
$manager1 = new LoggingManager(new SessionStoreManager(Cache::driver(), $sessionId1));
70+
$manager2 = new LoggingManager(new SessionStoreManager(Cache::driver(), $sessionId2));
71+
72+
expect($manager1->getLevel())->toBe(LogLevel::Debug)
73+
->and($manager2->getLevel())->toBe(LogLevel::Error);
74+
});

tests/Unit/ServerTest.php

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,21 @@
3232

3333
($transport->handler)($payload);
3434

35-
$jsonResponse = $transport->sent[0];
35+
$response = json_decode((string) $transport->sent[0], true);
3636

37-
$capabilities = (fn (): array => $this->capabilities)->call($server);
37+
expect($response)->toHaveKey('result.capabilities');
3838

39-
$expectedCapabilitiesJson = json_encode(array_merge($capabilities, [
40-
'customFeature' => [
41-
'enabled' => true,
42-
],
43-
'anotherFeature' => (object) [],
44-
]));
39+
$capabilities = $response['result']['capabilities'];
40+
41+
expect($capabilities)->toHaveKey('customFeature')
42+
->and($capabilities['customFeature'])->toBeArray()
43+
->and($capabilities['customFeature']['enabled'])->toBeTrue()
44+
->and($capabilities)->toHaveKey('anotherFeature')
45+
->and($capabilities['anotherFeature'])->toBeArray()
46+
->and($capabilities)->toHaveKey('tools')
47+
->and($capabilities)->toHaveKey('resources')
48+
->and($capabilities)->toHaveKey('prompts');
4549

46-
$this->assertStringContainsString($expectedCapabilitiesJson, $jsonResponse);
4750
});
4851

4952
it('can handle a list tools message', function (): void {

0 commit comments

Comments
 (0)