Skip to content

Commit a2d5000

Browse files
feat: throw exceptions from reference provider instead of returning null
1 parent 9ae7289 commit a2d5000

File tree

14 files changed

+126
-99
lines changed

14 files changed

+126
-99
lines changed

src/Capability/Registry.php

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
use Mcp\Event\ResourceTemplateListChangedEvent;
2424
use Mcp\Event\ToolListChangedEvent;
2525
use Mcp\Exception\InvalidCursorException;
26+
use Mcp\Exception\PromptNotFoundException;
27+
use Mcp\Exception\ResourceNotFoundException;
28+
use Mcp\Exception\ToolNotFoundException;
2629
use Mcp\Schema\Page;
2730
use Mcp\Schema\Prompt;
2831
use Mcp\Schema\Resource;
@@ -209,43 +212,41 @@ public function clear(): void
209212
}
210213
}
211214

212-
public function getTool(string $name): ?ToolReference
215+
public function getTool(string $name): ToolReference
213216
{
214-
return $this->tools[$name] ?? null;
217+
return $this->tools[$name] ?? throw new ToolNotFoundException($name);
215218
}
216219

217220
public function getResource(
218221
string $uri,
219222
bool $includeTemplates = true,
220-
): ResourceReference|ResourceTemplateReference|null {
223+
): ResourceReference|ResourceTemplateReference {
221224
$registration = $this->resources[$uri] ?? null;
222225
if ($registration) {
223226
return $registration;
224227
}
225228

226-
if (!$includeTemplates) {
227-
return null;
228-
}
229-
230-
foreach ($this->resourceTemplates as $template) {
231-
if ($template->matches($uri)) {
232-
return $template;
229+
if ($includeTemplates) {
230+
foreach ($this->resourceTemplates as $template) {
231+
if ($template->matches($uri)) {
232+
return $template;
233+
}
233234
}
234235
}
235236

236237
$this->logger->debug('No resource matched URI.', ['uri' => $uri]);
237238

238-
return null;
239+
throw new ResourceNotFoundException($uri);
239240
}
240241

241-
public function getResourceTemplate(string $uriTemplate): ?ResourceTemplateReference
242+
public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference
242243
{
243-
return $this->resourceTemplates[$uriTemplate] ?? null;
244+
return $this->resourceTemplates[$uriTemplate] ?? throw new ResourceNotFoundException($uriTemplate);
244245
}
245246

246-
public function getPrompt(string $name): ?PromptReference
247+
public function getPrompt(string $name): PromptReference
247248
{
248-
return $this->prompts[$name] ?? null;
249+
return $this->prompts[$name] ?? throw new PromptNotFoundException($name);
249250
}
250251

251252
public function getTools(?int $limit = null, ?string $cursor = null): Page

src/Capability/Registry/ReferenceProviderInterface.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Mcp\Capability\Registry;
1313

14+
use Mcp\Exception\PromptNotFoundException;
15+
use Mcp\Exception\ResourceNotFoundException;
16+
use Mcp\Exception\ToolNotFoundException;
1417
use Mcp\Schema\Page;
1518

1619
/**
@@ -23,23 +26,31 @@ interface ReferenceProviderInterface
2326
{
2427
/**
2528
* Gets a tool reference by name.
29+
*
30+
* @throws ToolNotFoundException
2631
*/
27-
public function getTool(string $name): ?ToolReference;
32+
public function getTool(string $name): ToolReference;
2833

2934
/**
3035
* Gets a resource reference by URI (includes template matching if enabled).
36+
*
37+
* @throws ResourceNotFoundException
3138
*/
32-
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference|null;
39+
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference;
3340

3441
/**
3542
* Gets a resource template reference by URI template.
43+
*
44+
* @throws ResourceNotFoundException
3645
*/
37-
public function getResourceTemplate(string $uriTemplate): ?ResourceTemplateReference;
46+
public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference;
3847

3948
/**
4049
* Gets a prompt reference by name.
50+
*
51+
* @throws PromptNotFoundException
4152
*/
42-
public function getPrompt(string $name): ?PromptReference;
53+
public function getPrompt(string $name): PromptReference;
4354

4455
/**
4556
* Gets all registered tools.

src/Exception/PromptNotFoundException.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,14 @@
1111

1212
namespace Mcp\Exception;
1313

14-
use Mcp\Schema\Request\GetPromptRequest;
15-
1614
/**
1715
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
1816
*/
1917
final class PromptNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
2018
{
2119
public function __construct(
22-
public readonly GetPromptRequest $request,
20+
public readonly string $name,
2321
) {
24-
parent::__construct(\sprintf('Prompt not found for name: "%s".', $request->name));
22+
parent::__construct(\sprintf('Prompt not found: "%s".', $name));
2523
}
2624
}

src/Exception/ResourceNotFoundException.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,14 @@
1111

1212
namespace Mcp\Exception;
1313

14-
use Mcp\Schema\Request\ReadResourceRequest;
15-
1614
/**
1715
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
1816
*/
1917
final class ResourceNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
2018
{
2119
public function __construct(
22-
public readonly ReadResourceRequest $request,
20+
public readonly string $uri,
2321
) {
24-
parent::__construct(\sprintf('Resource not found for uri: "%s".', $request->uri));
22+
parent::__construct(\sprintf('Resource not found for uri: "%s".', $uri));
2523
}
2624
}

src/Exception/ToolNotFoundException.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,14 @@
1111

1212
namespace Mcp\Exception;
1313

14-
use Mcp\Schema\Request\CallToolRequest;
15-
1614
/**
1715
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
1816
*/
1917
final class ToolNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
2018
{
2119
public function __construct(
22-
public readonly CallToolRequest $request,
20+
public readonly string $name,
2321
) {
24-
parent::__construct(\sprintf('Tool not found for call: "%s".', $request->name));
22+
parent::__construct(\sprintf('Tool not found: "%s".', $name));
2523
}
2624
}

src/Server/Handler/Request/CallToolHandler.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@ public function handle(Request $request, SessionInterface $session): Response|Er
5959

6060
try {
6161
$reference = $this->referenceProvider->getTool($toolName);
62-
if (null === $reference) {
63-
throw new ToolNotFoundException($request);
64-
}
6562

6663
$arguments['_session'] = $session;
6764

src/Server/Handler/Request/CompletionCompleteHandler.php

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@
1313

1414
use Mcp\Capability\Completion\ProviderInterface;
1515
use Mcp\Capability\Registry\ReferenceProviderInterface;
16+
use Mcp\Exception\PromptNotFoundException;
17+
use Mcp\Exception\ResourceNotFoundException;
1618
use Mcp\Schema\JsonRpc\Error;
1719
use Mcp\Schema\JsonRpc\Request;
1820
use Mcp\Schema\JsonRpc\Response;
21+
use Mcp\Schema\PromptReference;
1922
use Mcp\Schema\Request\CompletionCompleteRequest;
23+
use Mcp\Schema\ResourceReference;
2024
use Mcp\Schema\Result\CompletionCompleteResult;
2125
use Mcp\Server\Session\SessionInterface;
2226
use Psr\Container\ContainerInterface;
@@ -51,41 +55,38 @@ public function handle(Request $request, SessionInterface $session): Response|Er
5155
$name = $request->argument['name'] ?? '';
5256
$value = $request->argument['value'] ?? '';
5357

54-
$reference = match ($request->ref->type) {
55-
'ref/prompt' => $this->referenceProvider->getPrompt($request->ref->name),
56-
'ref/resource' => $this->referenceProvider->getResourceTemplate($request->ref->uri),
57-
default => null,
58-
};
59-
60-
if (null === $reference) {
61-
return new Response($request->getId(), new CompletionCompleteResult([]));
62-
}
58+
try {
59+
$reference = match (true) {
60+
$request->ref instanceof PromptReference => $this->referenceProvider->getPrompt($request->ref->name),
61+
$request->ref instanceof ResourceReference => $this->referenceProvider->getResource($request->ref->uri),
62+
};
6363

64-
$providers = $reference->completionProviders;
65-
$provider = $providers[$name] ?? null;
66-
if (null === $provider) {
67-
return new Response($request->getId(), new CompletionCompleteResult([]));
68-
}
64+
$providers = $reference->completionProviders;
65+
$provider = $providers[$name] ?? null;
66+
if (null === $provider) {
67+
return new Response($request->getId(), new CompletionCompleteResult([]));
68+
}
6969

70-
if (\is_string($provider)) {
71-
if (!class_exists($provider)) {
72-
return Error::forInternalError('Invalid completion provider', $request->getId());
70+
if (\is_string($provider)) {
71+
if (!class_exists($provider)) {
72+
return Error::forInternalError('Invalid completion provider', $request->getId());
73+
}
74+
$provider = $this->container?->has($provider) ? $this->container->get($provider) : new $provider();
7375
}
74-
$provider = $this->container?->has($provider) ? $this->container->get($provider) : new $provider();
75-
}
7676

77-
if (!$provider instanceof ProviderInterface) {
78-
return Error::forInternalError('Invalid completion provider type', $request->getId());
79-
}
77+
if (!$provider instanceof ProviderInterface) {
78+
return Error::forInternalError('Invalid completion provider type', $request->getId());
79+
}
8080

81-
try {
8281
$completions = $provider->getCompletions($value);
8382
$total = \count($completions);
8483
$hasMore = $total > 100;
8584
$paged = \array_slice($completions, 0, 100);
8685

8786
return new Response($request->getId(), new CompletionCompleteResult($paged, $total, $hasMore));
88-
} catch (\Throwable) {
87+
} catch (PromptNotFoundException|ResourceNotFoundException $e) {
88+
return Error::forResourceNotFound($e->getMessage(), $request->getId());
89+
} catch (\Throwable $e) {
8990
return Error::forInternalError('Error while handling completion request', $request->getId());
9091
}
9192
}

src/Server/Handler/Request/GetPromptHandler.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,6 @@ public function handle(Request $request, SessionInterface $session): Response|Er
5555

5656
try {
5757
$reference = $this->referenceProvider->getPrompt($promptName);
58-
if (null === $reference) {
59-
throw new PromptNotFoundException($request);
60-
}
6158

6259
$arguments['_session'] = $session;
6360

src/Server/Handler/Request/ReadResourceHandler.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ public function handle(Request $request, SessionInterface $session): Response|Er
5757

5858
try {
5959
$reference = $this->referenceProvider->getResource($uri);
60-
if (null === $reference) {
61-
throw new ResourceNotFoundException($request);
62-
}
6360

6461
$arguments = [
6562
'uri' => $uri,

tests/Unit/Capability/Registry/RegistryProviderTest.php

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use Mcp\Capability\Registry\ResourceReference;
1717
use Mcp\Capability\Registry\ResourceTemplateReference;
1818
use Mcp\Capability\Registry\ToolReference;
19+
use Mcp\Exception\PromptNotFoundException;
20+
use Mcp\Exception\ResourceNotFoundException;
21+
use Mcp\Exception\ToolNotFoundException;
1922
use Mcp\Schema\Prompt;
2023
use Mcp\Schema\Resource;
2124
use Mcp\Schema\ResourceTemplate;
@@ -45,10 +48,12 @@ public function testGetToolReturnsRegisteredTool(): void
4548
$this->assertFalse($toolRef->isManual);
4649
}
4750

48-
public function testGetToolReturnsNullForUnregisteredTool(): void
51+
public function testGetToolThrowsExceptionForUnregisteredTool(): void
4952
{
50-
$toolRef = $this->registry->getTool('non_existent_tool');
51-
$this->assertNull($toolRef);
53+
$this->expectException(ToolNotFoundException::class);
54+
$this->expectExceptionMessage('Tool not found: "non_existent_tool".');
55+
56+
$this->registry->getTool('non_existent_tool');
5257
}
5358

5459
public function testGetResourceReturnsRegisteredResource(): void
@@ -65,10 +70,12 @@ public function testGetResourceReturnsRegisteredResource(): void
6570
$this->assertFalse($resourceRef->isManual);
6671
}
6772

68-
public function testGetResourceReturnsNullForUnregisteredResource(): void
73+
public function testGetResourceThrowsExceptionForUnregisteredResource(): void
6974
{
70-
$resourceRef = $this->registry->getResource('test://non_existent');
71-
$this->assertNull($resourceRef);
75+
$this->expectException(ResourceNotFoundException::class);
76+
$this->expectExceptionMessage('Resource not found for uri: "test://non_existent".');
77+
78+
$this->registry->getResource('test://non_existent');
7279
}
7380

7481
public function testGetResourceMatchesResourceTemplate(): void
@@ -84,15 +91,17 @@ public function testGetResourceMatchesResourceTemplate(): void
8491
$this->assertEquals($handler, $resourceRef->handler);
8592
}
8693

87-
public function testGetResourceWithIncludeTemplatesFalse(): void
94+
public function testGetResourceWithIncludeTemplatesFalseThrowsException(): void
8895
{
8996
$template = $this->createValidResourceTemplate('test://{id}');
9097
$handler = fn (string $id) => "content for {$id}";
9198

9299
$this->registry->registerResourceTemplate($template, $handler);
93100

94-
$resourceRef = $this->registry->getResource('test://123', false);
95-
$this->assertNull($resourceRef);
101+
$this->expectException(ResourceNotFoundException::class);
102+
$this->expectExceptionMessage('Resource not found for uri: "test://123".');
103+
104+
$this->registry->getResource('test://123', false);
96105
}
97106

98107
public function testGetResourcePrefersDirectResourceOverTemplate(): void
@@ -125,10 +134,12 @@ public function testGetResourceTemplateReturnsRegisteredTemplate(): void
125134
$this->assertFalse($templateRef->isManual);
126135
}
127136

128-
public function testGetResourceTemplateReturnsNullForUnregisteredTemplate(): void
137+
public function testGetResourceTemplateThrowsExceptionForUnregisteredTemplate(): void
129138
{
130-
$templateRef = $this->registry->getResourceTemplate('test://{non_existent}');
131-
$this->assertNull($templateRef);
139+
$this->expectException(ResourceNotFoundException::class);
140+
$this->expectExceptionMessage('Resource not found for uri: "test://{non_existent}".');
141+
142+
$this->registry->getResourceTemplate('test://{non_existent}');
132143
}
133144

134145
public function testGetPromptReturnsRegisteredPrompt(): void
@@ -145,10 +156,12 @@ public function testGetPromptReturnsRegisteredPrompt(): void
145156
$this->assertFalse($promptRef->isManual);
146157
}
147158

148-
public function testGetPromptReturnsNullForUnregisteredPrompt(): void
159+
public function testGetPromptThrowsExceptionForUnregisteredPrompt(): void
149160
{
150-
$promptRef = $this->registry->getPrompt('non_existent_prompt');
151-
$this->assertNull($promptRef);
161+
$this->expectException(PromptNotFoundException::class);
162+
$this->expectExceptionMessage('Prompt not found: "non_existent_prompt".');
163+
164+
$this->registry->getPrompt('non_existent_prompt');
152165
}
153166

154167
public function testGetToolsReturnsAllRegisteredTools(): void

0 commit comments

Comments
 (0)