[Server] Simplify server extensibility with pluggable method handlers (#100)
* refactor: use a custom method handler registration system * refactor: remove redundant Completer and CompleterInterface * docs: update server-builder documentation to reflect custom method handler changes
This commit is contained in:
parent
e94fd82061
commit
fcf605d5d7
26 changed files with 896 additions and 2865 deletions
|
|
@ -12,7 +12,7 @@ various aspects of the server behavior.
|
||||||
- [Session Management](#session-management)
|
- [Session Management](#session-management)
|
||||||
- [Manual Capability Registration](#manual-capability-registration)
|
- [Manual Capability Registration](#manual-capability-registration)
|
||||||
- [Service Dependencies](#service-dependencies)
|
- [Service Dependencies](#service-dependencies)
|
||||||
- [Custom Capability Handlers](#custom-capability-handlers)
|
- [Custom Method Handlers](#custom-method-handlers)
|
||||||
- [Complete Example](#complete-example)
|
- [Complete Example](#complete-example)
|
||||||
- [Method Reference](#method-reference)
|
- [Method Reference](#method-reference)
|
||||||
|
|
||||||
|
|
@ -344,102 +344,50 @@ $server = Server::builder()
|
||||||
->setEventDispatcher($eventDispatcher);
|
->setEventDispatcher($eventDispatcher);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom Capability Handlers
|
## Custom Method Handlers
|
||||||
|
|
||||||
**Advanced customization for specific use cases.** Override the default capability handlers when you need completely custom
|
**Low-level escape hatch.** Custom method handlers run before the SDK’s built-in handlers and give you total control over
|
||||||
behavior for how tools are executed, resources are read, or prompts are generated. Most users should stick with the default implementations.
|
individual JSON-RPC methods. They do not receive the builder’s registry, container, or discovery output unless you pass
|
||||||
|
those dependencies in yourself.
|
||||||
|
|
||||||
The default handlers work by:
|
Attach handlers with `addMethodHandler()` (single) or `addMethodHandlers()` (multiple). You can call these methods as
|
||||||
1. Looking up registered tools/resources/prompts by name/URI
|
many times as needed; each call prepends the handlers so they execute before the defaults:
|
||||||
2. Resolving the handler from the container
|
|
||||||
3. Executing the handler with the provided arguments
|
|
||||||
4. Formatting the result and handling errors
|
|
||||||
|
|
||||||
### Custom Tool Caller
|
|
||||||
|
|
||||||
Replace how tool execution requests are processed. Your custom `ToolCallerInterface` receives a `CallToolRequest` (with
|
|
||||||
tool name and arguments) and must return a `CallToolResult`.
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Mcp\Capability\Tool\ToolCallerInterface;
|
|
||||||
use Mcp\Schema\Request\CallToolRequest;
|
|
||||||
use Mcp\Schema\Result\CallToolResult;
|
|
||||||
|
|
||||||
class CustomToolCaller implements ToolCallerInterface
|
|
||||||
{
|
|
||||||
public function call(CallToolRequest $request): CallToolResult
|
|
||||||
{
|
|
||||||
// Custom tool routing, execution, authentication, caching, etc.
|
|
||||||
// You handle finding the tool, executing it, and formatting results
|
|
||||||
$toolName = $request->name;
|
|
||||||
$arguments = $request->arguments ?? [];
|
|
||||||
|
|
||||||
// Your custom logic here
|
|
||||||
return new CallToolResult([/* content */]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$server = Server::builder()
|
$server = Server::builder()
|
||||||
->setToolCaller(new CustomToolCaller());
|
->addMethodHandler(new AuditHandler())
|
||||||
|
->addMethodHandlers([
|
||||||
|
new CustomListToolsHandler(),
|
||||||
|
new CustomCallToolHandler(),
|
||||||
|
])
|
||||||
|
->build();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Resource Reader
|
Custom handlers implement `MethodHandlerInterface`:
|
||||||
|
|
||||||
Replace how resource reading requests are processed. Your custom `ResourceReaderInterface` receives a `ReadResourceRequest`
|
|
||||||
(with URI) and must return a `ReadResourceResult`.
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Mcp\Capability\Resource\ResourceReaderInterface;
|
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\Request\ReadResourceRequest;
|
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||||
use Mcp\Schema\Result\ReadResourceResult;
|
use Mcp\Server\Session\SessionInterface;
|
||||||
|
|
||||||
class CustomResourceReader implements ResourceReaderInterface
|
interface MethodHandlerInterface
|
||||||
{
|
{
|
||||||
public function read(ReadResourceRequest $request): ReadResourceResult
|
public function supports(HasMethodInterface $message): bool;
|
||||||
{
|
|
||||||
// Custom resource resolution, caching, access control, etc.
|
|
||||||
$uri = $request->uri;
|
|
||||||
|
|
||||||
// Your custom logic here
|
public function handle(HasMethodInterface $message, SessionInterface $session);
|
||||||
return new ReadResourceResult([/* content */]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$server = Server::builder()
|
|
||||||
->setResourceReader(new CustomResourceReader());
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Prompt Getter
|
- `supports()` decides if the handler should look at the incoming message.
|
||||||
|
- `handle()` must return a JSON-RPC `Response`, an `Error`, or `null`.
|
||||||
|
|
||||||
Replace how prompt generation requests are processed. Your custom `PromptGetterInterface` receives a `GetPromptRequest`
|
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
|
||||||
(with prompt name and arguments) and must return a `GetPromptResult`.
|
custom `tool/list` and `tool/call` methods independently of the registry.
|
||||||
|
|
||||||
```php
|
> **Warning**: Custom method handlers bypass discovery, manual capability registration, and container lookups (unlesss
|
||||||
use Mcp\Capability\Prompt\PromptGetterInterface;
|
> you explicitly pass them). Tools, resources, and prompts you register elsewhere will not show up unless your handler
|
||||||
use Mcp\Schema\Request\GetPromptRequest;
|
> loads and executes them manually.
|
||||||
use Mcp\Schema\Result\GetPromptResult;
|
> Reach for this API only when you need that level of control and are comfortable taking on the additional plumbing.
|
||||||
|
|
||||||
class CustomPromptGetter implements PromptGetterInterface
|
|
||||||
{
|
|
||||||
public function get(GetPromptRequest $request): GetPromptResult
|
|
||||||
{
|
|
||||||
// Custom prompt generation, template engines, dynamic content, etc.
|
|
||||||
$promptName = $request->name;
|
|
||||||
$arguments = $request->arguments ?? [];
|
|
||||||
|
|
||||||
// Your custom logic here
|
|
||||||
return new GetPromptResult([/* messages */]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$server = Server::builder()
|
|
||||||
->setPromptGetter(new CustomPromptGetter());
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Warning**: Custom capability handlers bypass the entire default registration system (discovered attributes, manual
|
|
||||||
> registration, container resolution, etc.). You become responsible for all aspect of execution, including error handling,
|
|
||||||
> logging, and result formatting. Only use this for very specific advanced use cases like custom authentication, complex
|
|
||||||
> routing, or integration with external systems.
|
|
||||||
|
|
||||||
## Complete Example
|
## Complete Example
|
||||||
|
|
||||||
|
|
@ -505,9 +453,8 @@ $server = Server::builder()
|
||||||
| `setLogger()` | logger | Set PSR-3 logger |
|
| `setLogger()` | logger | Set PSR-3 logger |
|
||||||
| `setContainer()` | container | Set PSR-11 container |
|
| `setContainer()` | container | Set PSR-11 container |
|
||||||
| `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher |
|
| `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher |
|
||||||
| `setToolCaller()` | caller | Set custom tool caller |
|
| `addMethodHandler()` | handler | Prepend a single custom method handler |
|
||||||
| `setResourceReader()` | reader | Set custom resource reader |
|
| `addMethodHandlers()` | handlers | Prepend multiple custom method handlers |
|
||||||
| `setPromptGetter()` | getter | Set custom prompt getter |
|
|
||||||
| `addTool()` | handler, name?, description?, annotations?, inputSchema? | Register tool |
|
| `addTool()` | handler, name?, description?, annotations?, inputSchema? | Register tool |
|
||||||
| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource |
|
| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource |
|
||||||
| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template |
|
| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template |
|
||||||
|
|
|
||||||
144
examples/custom-method-handlers/server.php
Normal file
144
examples/custom-method-handlers/server.php
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the official PHP MCP SDK.
|
||||||
|
*
|
||||||
|
* A collaboration between Symfony and the PHP Foundation.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/bootstrap.php';
|
||||||
|
chdir(__DIR__);
|
||||||
|
|
||||||
|
use Mcp\Schema\Content\TextContent;
|
||||||
|
use Mcp\Schema\JsonRpc\Error;
|
||||||
|
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
|
use Mcp\Schema\Request\CallToolRequest;
|
||||||
|
use Mcp\Schema\Request\ListToolsRequest;
|
||||||
|
use Mcp\Schema\Result\CallToolResult;
|
||||||
|
use Mcp\Schema\Result\ListToolsResult;
|
||||||
|
use Mcp\Schema\ServerCapabilities;
|
||||||
|
use Mcp\Schema\Tool;
|
||||||
|
use Mcp\Server;
|
||||||
|
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||||
|
use Mcp\Server\Session\SessionInterface;
|
||||||
|
use Mcp\Server\Transport\StdioTransport;
|
||||||
|
|
||||||
|
logger()->info('Starting MCP Custom Method Handlers (Stdio) Server...');
|
||||||
|
|
||||||
|
$toolDefinitions = [
|
||||||
|
'say_hello' => new Tool(
|
||||||
|
name: 'say_hello',
|
||||||
|
inputSchema: [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'name' => ['type' => 'string', 'description' => 'Name to greet'],
|
||||||
|
],
|
||||||
|
'required' => ['name'],
|
||||||
|
],
|
||||||
|
description: 'Greets a user by name.',
|
||||||
|
annotations: null,
|
||||||
|
),
|
||||||
|
'sum' => new Tool(
|
||||||
|
name: 'sum',
|
||||||
|
inputSchema: [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'a' => ['type' => 'number'],
|
||||||
|
'b' => ['type' => 'number'],
|
||||||
|
],
|
||||||
|
'required' => ['a', 'b'],
|
||||||
|
],
|
||||||
|
description: 'Returns a+b.',
|
||||||
|
annotations: null,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
$listToolsHandler = new class($toolDefinitions) implements MethodHandlerInterface {
|
||||||
|
/**
|
||||||
|
* @param array<string, Tool> $toolDefinitions
|
||||||
|
*/
|
||||||
|
public function __construct(private array $toolDefinitions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(HasMethodInterface $message): bool
|
||||||
|
{
|
||||||
|
return $message instanceof ListToolsRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(ListToolsRequest|HasMethodInterface $message, SessionInterface $session): Response
|
||||||
|
{
|
||||||
|
assert($message instanceof ListToolsRequest);
|
||||||
|
|
||||||
|
return new Response($message->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$callToolHandler = new class($toolDefinitions) implements MethodHandlerInterface {
|
||||||
|
/**
|
||||||
|
* @param array<string, Tool> $toolDefinitions
|
||||||
|
*/
|
||||||
|
public function __construct(private array $toolDefinitions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(HasMethodInterface $message): bool
|
||||||
|
{
|
||||||
|
return $message instanceof CallToolRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(CallToolRequest|HasMethodInterface $message, SessionInterface $session): Response|Error
|
||||||
|
{
|
||||||
|
assert($message instanceof CallToolRequest);
|
||||||
|
|
||||||
|
$name = $message->name;
|
||||||
|
$args = $message->arguments ?? [];
|
||||||
|
|
||||||
|
if (!isset($this->toolDefinitions[$name])) {
|
||||||
|
return new Error($message->getId(), Error::METHOD_NOT_FOUND, sprintf('Tool not found: %s', $name));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($name) {
|
||||||
|
case 'say_hello':
|
||||||
|
$greetName = (string) ($args['name'] ?? 'world');
|
||||||
|
$result = [new TextContent(sprintf('Hello, %s!', $greetName))];
|
||||||
|
break;
|
||||||
|
case 'sum':
|
||||||
|
$a = (float) ($args['a'] ?? 0);
|
||||||
|
$b = (float) ($args['b'] ?? 0);
|
||||||
|
$result = [new TextContent((string) ($a + $b))];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$result = [new TextContent('Unknown tool')];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response($message->getId(), new CallToolResult($result));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return new Response($message->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$capabilities = new ServerCapabilities(tools: true, resources: false, prompts: false);
|
||||||
|
|
||||||
|
$server = Server::builder()
|
||||||
|
->setServerInfo('Custom Handlers Server', '1.0.0')
|
||||||
|
->setLogger(logger())
|
||||||
|
->setContainer(container())
|
||||||
|
->setCapabilities($capabilities)
|
||||||
|
->addMethodHandlers([$listToolsHandler, $callToolHandler])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$transport = new StdioTransport(logger: logger());
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$transport->listen();
|
||||||
|
|
||||||
|
logger()->info('Server listener stopped gracefully.');
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Capability\Completion;
|
|
||||||
|
|
||||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|
||||||
use Mcp\Exception\RuntimeException;
|
|
||||||
use Mcp\Schema\Request\CompletionCompleteRequest;
|
|
||||||
use Mcp\Schema\Result\CompletionCompleteResult;
|
|
||||||
use Psr\Container\ContainerInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
|
||||||
*/
|
|
||||||
final class Completer implements CompleterInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly ReferenceProviderInterface $referenceProvider,
|
|
||||||
private readonly ?ContainerInterface $container = null,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function complete(CompletionCompleteRequest $request): CompletionCompleteResult
|
|
||||||
{
|
|
||||||
$argumentName = $request->argument['name'] ?? '';
|
|
||||||
$currentValue = $request->argument['value'] ?? '';
|
|
||||||
|
|
||||||
$reference = match (true) {
|
|
||||||
'ref/prompt' === $request->ref->type => $this->referenceProvider->getPrompt($request->ref->name),
|
|
||||||
'ref/resource' === $request->ref->type => $this->referenceProvider->getResourceTemplate($request->ref->uri),
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (null === $reference) {
|
|
||||||
return new CompletionCompleteResult([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$providerClassOrInstance = $reference->completionProviders[$argumentName] ?? null;
|
|
||||||
if (null === $providerClassOrInstance) {
|
|
||||||
return new CompletionCompleteResult([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (\is_string($providerClassOrInstance)) {
|
|
||||||
if (!class_exists($providerClassOrInstance)) {
|
|
||||||
throw new RuntimeException(\sprintf('Completion provider class "%s" does not exist.', $providerClassOrInstance));
|
|
||||||
}
|
|
||||||
|
|
||||||
$provider = $this->container?->has($providerClassOrInstance)
|
|
||||||
? $this->container->get($providerClassOrInstance)
|
|
||||||
: new $providerClassOrInstance();
|
|
||||||
} else {
|
|
||||||
$provider = $providerClassOrInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$provider instanceof ProviderInterface) {
|
|
||||||
throw new RuntimeException('Completion provider must implement ProviderInterface.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$completions = $provider->getCompletions($currentValue);
|
|
||||||
|
|
||||||
$total = \count($completions);
|
|
||||||
$hasMore = $total > 100;
|
|
||||||
$pagedCompletions = \array_slice($completions, 0, 100);
|
|
||||||
|
|
||||||
return new CompletionCompleteResult($pagedCompletions, $total, $hasMore);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Capability\Completion;
|
|
||||||
|
|
||||||
use Mcp\Schema\Request\CompletionCompleteRequest;
|
|
||||||
use Mcp\Schema\Result\CompletionCompleteResult;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides completion options for prompt arguments and resource template variables.
|
|
||||||
*
|
|
||||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
|
||||||
*/
|
|
||||||
interface CompleterInterface
|
|
||||||
{
|
|
||||||
public function complete(CompletionCompleteRequest $request): CompletionCompleteResult;
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Capability\Prompt;
|
|
||||||
|
|
||||||
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
|
||||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|
||||||
use Mcp\Exception\PromptGetException;
|
|
||||||
use Mcp\Exception\PromptNotFoundException;
|
|
||||||
use Mcp\Schema\Request\GetPromptRequest;
|
|
||||||
use Mcp\Schema\Result\GetPromptResult;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Pavel Buchnev <butschster@gmail.com>
|
|
||||||
*/
|
|
||||||
final class PromptGetter implements PromptGetterInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly ReferenceProviderInterface $referenceProvider,
|
|
||||||
private readonly ReferenceHandlerInterface $referenceHandler,
|
|
||||||
private readonly LoggerInterface $logger = new NullLogger(),
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get(GetPromptRequest $request): GetPromptResult
|
|
||||||
{
|
|
||||||
$promptName = $request->name;
|
|
||||||
$arguments = $request->arguments ?? [];
|
|
||||||
|
|
||||||
$this->logger->debug('Getting prompt', ['name' => $promptName, 'arguments' => $arguments]);
|
|
||||||
|
|
||||||
$reference = $this->referenceProvider->getPrompt($promptName);
|
|
||||||
|
|
||||||
if (null === $reference) {
|
|
||||||
$this->logger->warning('Prompt not found', ['name' => $promptName]);
|
|
||||||
throw new PromptNotFoundException($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = $this->referenceHandler->handle($reference, $arguments);
|
|
||||||
$formattedResult = $reference->formatResult($result);
|
|
||||||
|
|
||||||
$this->logger->debug('Prompt retrieved successfully', [
|
|
||||||
'name' => $promptName,
|
|
||||||
'result_type' => \gettype($result),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return new GetPromptResult($formattedResult);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->logger->error('Prompt retrieval failed', [
|
|
||||||
'name' => $promptName,
|
|
||||||
'exception' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw new PromptGetException($request, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Capability\Prompt;
|
|
||||||
|
|
||||||
use Mcp\Exception\PromptGetException;
|
|
||||||
use Mcp\Exception\PromptNotFoundException;
|
|
||||||
use Mcp\Schema\Request\GetPromptRequest;
|
|
||||||
use Mcp\Schema\Result\GetPromptResult;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
|
||||||
*/
|
|
||||||
interface PromptGetterInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @throws PromptGetException if the prompt execution fails
|
|
||||||
* @throws PromptNotFoundException if the prompt is not found
|
|
||||||
*/
|
|
||||||
public function get(GetPromptRequest $request): GetPromptResult;
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Capability\Resource;
|
|
||||||
|
|
||||||
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
|
||||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|
||||||
use Mcp\Exception\ResourceNotFoundException;
|
|
||||||
use Mcp\Exception\ResourceReadException;
|
|
||||||
use Mcp\Schema\Request\ReadResourceRequest;
|
|
||||||
use Mcp\Schema\Result\ReadResourceResult;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Pavel Buchnev <butschster@gmail.com>
|
|
||||||
*/
|
|
||||||
final class ResourceReader implements ResourceReaderInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly ReferenceProviderInterface $referenceProvider,
|
|
||||||
private readonly ReferenceHandlerInterface $referenceHandler,
|
|
||||||
private readonly LoggerInterface $logger = new NullLogger(),
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function read(ReadResourceRequest $request): ReadResourceResult
|
|
||||||
{
|
|
||||||
$uri = $request->uri;
|
|
||||||
|
|
||||||
$this->logger->debug('Reading resource', ['uri' => $uri]);
|
|
||||||
|
|
||||||
$reference = $this->referenceProvider->getResource($uri);
|
|
||||||
|
|
||||||
if (null === $reference) {
|
|
||||||
$this->logger->warning('Resource not found', ['uri' => $uri]);
|
|
||||||
throw new ResourceNotFoundException($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = $this->referenceHandler->handle($reference, ['uri' => $uri]);
|
|
||||||
$formattedResult = $reference->formatResult($result, $uri);
|
|
||||||
|
|
||||||
$this->logger->debug('Resource read successfully', [
|
|
||||||
'uri' => $uri,
|
|
||||||
'result_type' => \gettype($result),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return new ReadResourceResult($formattedResult);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->logger->error('Resource read failed', [
|
|
||||||
'uri' => $uri,
|
|
||||||
'exception' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw new ResourceReadException($request, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Capability\Resource;
|
|
||||||
|
|
||||||
use Mcp\Exception\ResourceNotFoundException;
|
|
||||||
use Mcp\Exception\ResourceReadException;
|
|
||||||
use Mcp\Schema\Request\ReadResourceRequest;
|
|
||||||
use Mcp\Schema\Result\ReadResourceResult;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
|
||||||
*/
|
|
||||||
interface ResourceReaderInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @throws ResourceReadException if the resource execution fails
|
|
||||||
* @throws ResourceNotFoundException if the resource is not found
|
|
||||||
*/
|
|
||||||
public function read(ReadResourceRequest $request): ReadResourceResult;
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Capability\Tool;
|
|
||||||
|
|
||||||
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
|
||||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|
||||||
use Mcp\Exception\ToolCallException;
|
|
||||||
use Mcp\Exception\ToolNotFoundException;
|
|
||||||
use Mcp\Schema\Content\AudioContent;
|
|
||||||
use Mcp\Schema\Content\EmbeddedResource;
|
|
||||||
use Mcp\Schema\Content\ImageContent;
|
|
||||||
use Mcp\Schema\Content\TextContent;
|
|
||||||
use Mcp\Schema\Request\CallToolRequest;
|
|
||||||
use Mcp\Schema\Result\CallToolResult;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default implementation of ToolCallerInterface that uses ReferenceProvider
|
|
||||||
* and ReferenceHandlerInterface to execute tools.
|
|
||||||
*
|
|
||||||
* @author Pavel Buchnev <butschster@gmail.com>
|
|
||||||
*/
|
|
||||||
final class ToolCaller implements ToolCallerInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly ReferenceProviderInterface $referenceProvider,
|
|
||||||
private readonly ReferenceHandlerInterface $referenceHandler,
|
|
||||||
private readonly LoggerInterface $logger = new NullLogger(),
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws ToolCallException if the tool execution fails
|
|
||||||
* @throws ToolNotFoundException if the tool is not found
|
|
||||||
*/
|
|
||||||
public function call(CallToolRequest $request): CallToolResult
|
|
||||||
{
|
|
||||||
$toolName = $request->name;
|
|
||||||
$arguments = $request->arguments ?? [];
|
|
||||||
|
|
||||||
$this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]);
|
|
||||||
|
|
||||||
$toolReference = $this->referenceProvider->getTool($toolName);
|
|
||||||
|
|
||||||
if (null === $toolReference) {
|
|
||||||
$this->logger->warning('Tool not found', ['name' => $toolName]);
|
|
||||||
throw new ToolNotFoundException($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = $this->referenceHandler->handle($toolReference, $arguments);
|
|
||||||
/** @var TextContent[]|ImageContent[]|EmbeddedResource[]|AudioContent[] $formattedResult */
|
|
||||||
$formattedResult = $toolReference->formatResult($result);
|
|
||||||
|
|
||||||
$this->logger->debug('Tool executed successfully', [
|
|
||||||
'name' => $toolName,
|
|
||||||
'result_type' => \gettype($result),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return new CallToolResult($formattedResult);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->logger->error('Tool execution failed', [
|
|
||||||
'name' => $toolName,
|
|
||||||
'exception' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw new ToolCallException($request, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Capability\Tool;
|
|
||||||
|
|
||||||
use Mcp\Exception\ToolCallException;
|
|
||||||
use Mcp\Exception\ToolNotFoundException;
|
|
||||||
use Mcp\Schema\Request\CallToolRequest;
|
|
||||||
use Mcp\Schema\Result\CallToolResult;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
|
||||||
*/
|
|
||||||
interface ToolCallerInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @throws ToolCallException if the tool execution fails
|
|
||||||
* @throws ToolNotFoundException if the tool is not found
|
|
||||||
*/
|
|
||||||
public function call(CallToolRequest $request): CallToolResult;
|
|
||||||
}
|
|
||||||
|
|
@ -17,7 +17,6 @@ use Mcp\Schema\Content\Content;
|
||||||
use Mcp\Schema\Content\EmbeddedResource;
|
use Mcp\Schema\Content\EmbeddedResource;
|
||||||
use Mcp\Schema\Content\ImageContent;
|
use Mcp\Schema\Content\ImageContent;
|
||||||
use Mcp\Schema\Content\TextContent;
|
use Mcp\Schema\Content\TextContent;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
|
||||||
use Mcp\Schema\JsonRpc\ResultInterface;
|
use Mcp\Schema\JsonRpc\ResultInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,11 +31,6 @@ use Mcp\Schema\JsonRpc\ResultInterface;
|
||||||
* server does not support tool calls, or any other exceptional conditions,
|
* server does not support tool calls, or any other exceptional conditions,
|
||||||
* should be reported as an MCP error response.
|
* should be reported as an MCP error response.
|
||||||
*
|
*
|
||||||
* @phpstan-import-type TextContentData from TextContent
|
|
||||||
* @phpstan-import-type ImageContentData from ImageContent
|
|
||||||
* @phpstan-import-type AudioContentData from AudioContent
|
|
||||||
* @phpstan-import-type EmbeddedResourceData from EmbeddedResource
|
|
||||||
*
|
|
||||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||||
*/
|
*/
|
||||||
class CallToolResult implements ResultInterface
|
class CallToolResult implements ResultInterface
|
||||||
|
|
@ -44,8 +38,8 @@ class CallToolResult implements ResultInterface
|
||||||
/**
|
/**
|
||||||
* Create a new CallToolResult.
|
* Create a new CallToolResult.
|
||||||
*
|
*
|
||||||
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
|
* @param Content[] $content The content of the tool result
|
||||||
* @param bool $isError Whether the tool execution resulted in an error. If not set, this is assumed to be false (the call was successful).
|
* @param bool $isError Whether the tool execution resulted in an error. If not set, this is assumed to be false (the call was successful).
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly array $content,
|
public readonly array $content,
|
||||||
|
|
@ -61,7 +55,7 @@ class CallToolResult implements ResultInterface
|
||||||
/**
|
/**
|
||||||
* Create a new CallToolResult with success status.
|
* Create a new CallToolResult with success status.
|
||||||
*
|
*
|
||||||
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
|
* @param Content[] $content The content of the tool result
|
||||||
*/
|
*/
|
||||||
public static function success(array $content): self
|
public static function success(array $content): self
|
||||||
{
|
{
|
||||||
|
|
@ -71,7 +65,7 @@ class CallToolResult implements ResultInterface
|
||||||
/**
|
/**
|
||||||
* Create a new CallToolResult with error status.
|
* Create a new CallToolResult with error status.
|
||||||
*
|
*
|
||||||
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
|
* @param Content[] $content The content of the tool result
|
||||||
*/
|
*/
|
||||||
public static function error(array $content): self
|
public static function error(array $content): self
|
||||||
{
|
{
|
||||||
|
|
@ -80,7 +74,7 @@ class CallToolResult implements ResultInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{
|
* @param array{
|
||||||
* content: array<TextContentData|ImageContentData|AudioContentData|EmbeddedResourceData>,
|
* content: array<mixed>,
|
||||||
* isError?: bool,
|
* isError?: bool,
|
||||||
* } $data
|
* } $data
|
||||||
*/
|
*/
|
||||||
|
|
@ -107,7 +101,7 @@ class CallToolResult implements ResultInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
* content: array<TextContent|ImageContent|AudioContent|EmbeddedResource>,
|
* content: array<mixed>,
|
||||||
* isError: bool,
|
* isError: bool,
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -20,27 +20,24 @@ use Mcp\Capability\Discovery\Discoverer;
|
||||||
use Mcp\Capability\Discovery\DocBlockParser;
|
use Mcp\Capability\Discovery\DocBlockParser;
|
||||||
use Mcp\Capability\Discovery\HandlerResolver;
|
use Mcp\Capability\Discovery\HandlerResolver;
|
||||||
use Mcp\Capability\Discovery\SchemaGenerator;
|
use Mcp\Capability\Discovery\SchemaGenerator;
|
||||||
use Mcp\Capability\Prompt\PromptGetter;
|
|
||||||
use Mcp\Capability\Prompt\PromptGetterInterface;
|
|
||||||
use Mcp\Capability\Registry;
|
use Mcp\Capability\Registry;
|
||||||
use Mcp\Capability\Registry\Container;
|
use Mcp\Capability\Registry\Container;
|
||||||
use Mcp\Capability\Registry\ElementReference;
|
use Mcp\Capability\Registry\ElementReference;
|
||||||
use Mcp\Capability\Registry\ReferenceHandler;
|
use Mcp\Capability\Registry\ReferenceHandler;
|
||||||
use Mcp\Capability\Resource\ResourceReader;
|
|
||||||
use Mcp\Capability\Resource\ResourceReaderInterface;
|
|
||||||
use Mcp\Capability\Tool\ToolCaller;
|
|
||||||
use Mcp\Capability\Tool\ToolCallerInterface;
|
|
||||||
use Mcp\Exception\ConfigurationException;
|
use Mcp\Exception\ConfigurationException;
|
||||||
|
use Mcp\JsonRpc\MessageFactory;
|
||||||
use Mcp\Schema\Annotations;
|
use Mcp\Schema\Annotations;
|
||||||
use Mcp\Schema\Implementation;
|
use Mcp\Schema\Implementation;
|
||||||
use Mcp\Schema\Prompt;
|
use Mcp\Schema\Prompt;
|
||||||
use Mcp\Schema\PromptArgument;
|
use Mcp\Schema\PromptArgument;
|
||||||
use Mcp\Schema\Resource;
|
use Mcp\Schema\Resource;
|
||||||
use Mcp\Schema\ResourceTemplate;
|
use Mcp\Schema\ResourceTemplate;
|
||||||
|
use Mcp\Schema\ServerCapabilities;
|
||||||
use Mcp\Schema\Tool;
|
use Mcp\Schema\Tool;
|
||||||
use Mcp\Schema\ToolAnnotations;
|
use Mcp\Schema\ToolAnnotations;
|
||||||
use Mcp\Server;
|
use Mcp\Server;
|
||||||
use Mcp\Server\Handler\JsonRpcHandler;
|
use Mcp\Server\Handler\JsonRpcHandler;
|
||||||
|
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||||
use Mcp\Server\Session\InMemorySessionStore;
|
use Mcp\Server\Session\InMemorySessionStore;
|
||||||
use Mcp\Server\Session\SessionFactory;
|
use Mcp\Server\Session\SessionFactory;
|
||||||
use Mcp\Server\Session\SessionFactoryInterface;
|
use Mcp\Server\Session\SessionFactoryInterface;
|
||||||
|
|
@ -64,12 +61,6 @@ final class Builder
|
||||||
|
|
||||||
private ?CacheInterface $discoveryCache = null;
|
private ?CacheInterface $discoveryCache = null;
|
||||||
|
|
||||||
private ?ToolCallerInterface $toolCaller = null;
|
|
||||||
|
|
||||||
private ?ResourceReaderInterface $resourceReader = null;
|
|
||||||
|
|
||||||
private ?PromptGetterInterface $promptGetter = null;
|
|
||||||
|
|
||||||
private ?EventDispatcherInterface $eventDispatcher = null;
|
private ?EventDispatcherInterface $eventDispatcher = null;
|
||||||
|
|
||||||
private ?ContainerInterface $container = null;
|
private ?ContainerInterface $container = null;
|
||||||
|
|
@ -84,6 +75,13 @@ final class Builder
|
||||||
|
|
||||||
private ?string $instructions = null;
|
private ?string $instructions = null;
|
||||||
|
|
||||||
|
private ?ServerCapabilities $explicitCapabilities = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int, MethodHandlerInterface>
|
||||||
|
*/
|
||||||
|
private array $customMethodHandlers = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array{
|
* @var array{
|
||||||
* handler: Handler,
|
* handler: Handler,
|
||||||
|
|
@ -174,6 +172,40 @@ final class Builder
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly set server capabilities. If set, this overrides automatic detection.
|
||||||
|
*/
|
||||||
|
public function setCapabilities(ServerCapabilities $capabilities): self
|
||||||
|
{
|
||||||
|
$this->explicitCapabilities = $capabilities;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a single custom method handler.
|
||||||
|
*/
|
||||||
|
public function addMethodHandler(MethodHandlerInterface $handler): self
|
||||||
|
{
|
||||||
|
$this->customMethodHandlers[] = $handler;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register multiple custom method handlers.
|
||||||
|
*
|
||||||
|
* @param iterable<int, MethodHandlerInterface> $handlers
|
||||||
|
*/
|
||||||
|
public function addMethodHandlers(iterable $handlers): self
|
||||||
|
{
|
||||||
|
foreach ($handlers as $handler) {
|
||||||
|
$this->customMethodHandlers[] = $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a PSR-3 logger instance. Defaults to NullLogger.
|
* Provides a PSR-3 logger instance. Defaults to NullLogger.
|
||||||
*/
|
*/
|
||||||
|
|
@ -191,27 +223,6 @@ final class Builder
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setToolCaller(ToolCallerInterface $toolCaller): self
|
|
||||||
{
|
|
||||||
$this->toolCaller = $toolCaller;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setResourceReader(ResourceReaderInterface $resourceReader): self
|
|
||||||
{
|
|
||||||
$this->resourceReader = $resourceReader;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setPromptGetter(PromptGetterInterface $promptGetter): self
|
|
||||||
{
|
|
||||||
$this->promptGetter = $promptGetter;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a PSR-11 DI container, primarily for resolving user-defined handler classes.
|
* Provides a PSR-11 DI container, primarily for resolving user-defined handler classes.
|
||||||
* Defaults to a basic internal container.
|
* Defaults to a basic internal container.
|
||||||
|
|
@ -333,49 +344,60 @@ final class Builder
|
||||||
public function build(): Server
|
public function build(): Server
|
||||||
{
|
{
|
||||||
$logger = $this->logger ?? new NullLogger();
|
$logger = $this->logger ?? new NullLogger();
|
||||||
|
|
||||||
$container = $this->container ?? new Container();
|
$container = $this->container ?? new Container();
|
||||||
$registry = new Registry($this->eventDispatcher, $logger);
|
$registry = new Registry($this->eventDispatcher, $logger);
|
||||||
|
|
||||||
$referenceHandler = new ReferenceHandler($container);
|
|
||||||
$toolCaller = $this->toolCaller ??= new ToolCaller($registry, $referenceHandler, $logger);
|
|
||||||
$resourceReader = $this->resourceReader ??= new ResourceReader($registry, $referenceHandler, $logger);
|
|
||||||
$promptGetter = $this->promptGetter ??= new PromptGetter($registry, $referenceHandler, $logger);
|
|
||||||
|
|
||||||
$this->registerCapabilities($registry, $logger);
|
$this->registerCapabilities($registry, $logger);
|
||||||
|
|
||||||
if (null !== $this->discoveryBasePath) {
|
if (null !== $this->discoveryBasePath) {
|
||||||
$discovery = new Discoverer($registry, $logger);
|
$this->performDiscovery($registry, $logger);
|
||||||
|
|
||||||
if (null !== $this->discoveryCache) {
|
|
||||||
$discovery = new CachedDiscoverer($discovery, $this->discoveryCache, $logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
$discovery->discover($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$sessionTtl = $this->sessionTtl ?? 3600;
|
$sessionTtl = $this->sessionTtl ?? 3600;
|
||||||
$sessionFactory = $this->sessionFactory ?? new SessionFactory();
|
$sessionFactory = $this->sessionFactory ?? new SessionFactory();
|
||||||
$sessionStore = $this->sessionStore ?? new InMemorySessionStore($sessionTtl);
|
$sessionStore = $this->sessionStore ?? new InMemorySessionStore($sessionTtl);
|
||||||
|
$messageFactory = MessageFactory::make();
|
||||||
|
|
||||||
return new Server(
|
$capabilities = $this->explicitCapabilities ?? $registry->getCapabilities();
|
||||||
jsonRpcHandler: JsonRpcHandler::make(
|
$configuration = new Configuration($this->serverInfo, $capabilities, $this->paginationLimit, $this->instructions);
|
||||||
registry: $registry,
|
$referenceHandler = new ReferenceHandler($container);
|
||||||
referenceProvider: $registry,
|
|
||||||
configuration: new Configuration(
|
$methodHandlers = array_merge($this->customMethodHandlers, [
|
||||||
$this->serverInfo,
|
new Handler\Request\PingHandler(),
|
||||||
$registry->getCapabilities(),
|
new Handler\Request\InitializeHandler($configuration),
|
||||||
$this->paginationLimit, $this->instructions,
|
new Handler\Request\ListToolsHandler($registry, $this->paginationLimit),
|
||||||
),
|
new Handler\Request\CallToolHandler($registry, $referenceHandler, $logger),
|
||||||
toolCaller: $toolCaller,
|
new Handler\Request\ListResourcesHandler($registry, $this->paginationLimit),
|
||||||
resourceReader: $resourceReader,
|
new Handler\Request\ListResourceTemplatesHandler($registry, $this->paginationLimit),
|
||||||
promptGetter: $promptGetter,
|
new Handler\Request\ReadResourceHandler($registry, $referenceHandler, $logger),
|
||||||
sessionStore: $sessionStore,
|
new Handler\Request\ListPromptsHandler($registry, $this->paginationLimit),
|
||||||
sessionFactory: $sessionFactory,
|
new Handler\Request\GetPromptHandler($registry, $referenceHandler, $logger),
|
||||||
logger: $logger,
|
|
||||||
),
|
new Handler\Notification\InitializedHandler(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$jsonRpcHandler = new JsonRpcHandler(
|
||||||
|
methodHandlers: $methodHandlers,
|
||||||
|
messageFactory: $messageFactory,
|
||||||
|
sessionFactory: $sessionFactory,
|
||||||
|
sessionStore: $sessionStore,
|
||||||
logger: $logger,
|
logger: $logger,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return new Server($jsonRpcHandler, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function performDiscovery(
|
||||||
|
Registry\ReferenceRegistryInterface $registry,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
): void {
|
||||||
|
$discovery = new Discoverer($registry, $logger);
|
||||||
|
|
||||||
|
if (null !== $this->discoveryCache) {
|
||||||
|
$discovery = new CachedDiscoverer($discovery, $this->discoveryCache, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
$discovery->discover($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,6 @@
|
||||||
|
|
||||||
namespace Mcp\Server\Handler;
|
namespace Mcp\Server\Handler;
|
||||||
|
|
||||||
use Mcp\Capability\Completion\Completer;
|
|
||||||
use Mcp\Capability\Prompt\PromptGetterInterface;
|
|
||||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|
||||||
use Mcp\Capability\Registry\ReferenceRegistryInterface;
|
|
||||||
use Mcp\Capability\Resource\ResourceReaderInterface;
|
|
||||||
use Mcp\Capability\Tool\ToolCallerInterface;
|
|
||||||
use Mcp\Exception\ExceptionInterface;
|
use Mcp\Exception\ExceptionInterface;
|
||||||
use Mcp\Exception\HandlerNotFoundException;
|
use Mcp\Exception\HandlerNotFoundException;
|
||||||
use Mcp\Exception\InvalidInputMessageException;
|
use Mcp\Exception\InvalidInputMessageException;
|
||||||
|
|
@ -27,8 +21,6 @@ use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\JsonRpc\Request;
|
use Mcp\Schema\JsonRpc\Request;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
use Mcp\Schema\Request\InitializeRequest;
|
use Mcp\Schema\Request\InitializeRequest;
|
||||||
use Mcp\Server\Configuration;
|
|
||||||
use Mcp\Server\Handler;
|
|
||||||
use Mcp\Server\Session\SessionFactoryInterface;
|
use Mcp\Server\Session\SessionFactoryInterface;
|
||||||
use Mcp\Server\Session\SessionInterface;
|
use Mcp\Server\Session\SessionInterface;
|
||||||
use Mcp\Server\Session\SessionStoreInterface;
|
use Mcp\Server\Session\SessionStoreInterface;
|
||||||
|
|
@ -44,56 +36,15 @@ use Symfony\Component\Uid\Uuid;
|
||||||
class JsonRpcHandler
|
class JsonRpcHandler
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array<int, MethodHandlerInterface>
|
* @param array<int, MethodHandlerInterface> $methodHandlers
|
||||||
*/
|
|
||||||
private readonly array $methodHandlers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param iterable<int, MethodHandlerInterface> $methodHandlers
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly array $methodHandlers,
|
||||||
private readonly MessageFactory $messageFactory,
|
private readonly MessageFactory $messageFactory,
|
||||||
private readonly SessionFactoryInterface $sessionFactory,
|
private readonly SessionFactoryInterface $sessionFactory,
|
||||||
private readonly SessionStoreInterface $sessionStore,
|
private readonly SessionStoreInterface $sessionStore,
|
||||||
iterable $methodHandlers,
|
|
||||||
private readonly LoggerInterface $logger = new NullLogger(),
|
private readonly LoggerInterface $logger = new NullLogger(),
|
||||||
) {
|
) {
|
||||||
$this->methodHandlers = $methodHandlers instanceof \Traversable ? iterator_to_array(
|
|
||||||
$methodHandlers,
|
|
||||||
) : $methodHandlers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function make(
|
|
||||||
ReferenceRegistryInterface $registry,
|
|
||||||
ReferenceProviderInterface $referenceProvider,
|
|
||||||
Configuration $configuration,
|
|
||||||
ToolCallerInterface $toolCaller,
|
|
||||||
ResourceReaderInterface $resourceReader,
|
|
||||||
PromptGetterInterface $promptGetter,
|
|
||||||
SessionStoreInterface $sessionStore,
|
|
||||||
SessionFactoryInterface $sessionFactory,
|
|
||||||
LoggerInterface $logger = new NullLogger(),
|
|
||||||
int $paginationLimit = 50,
|
|
||||||
): self {
|
|
||||||
return new self(
|
|
||||||
messageFactory: MessageFactory::make(),
|
|
||||||
sessionFactory: $sessionFactory,
|
|
||||||
sessionStore: $sessionStore,
|
|
||||||
methodHandlers: [
|
|
||||||
new Notification\InitializedHandler(),
|
|
||||||
new Handler\Request\InitializeHandler($configuration),
|
|
||||||
new Handler\Request\PingHandler(),
|
|
||||||
new Handler\Request\ListPromptsHandler($referenceProvider, $paginationLimit),
|
|
||||||
new Handler\Request\GetPromptHandler($promptGetter),
|
|
||||||
new Handler\Request\ListResourcesHandler($referenceProvider, $paginationLimit),
|
|
||||||
new Handler\Request\ReadResourceHandler($resourceReader),
|
|
||||||
new Handler\Request\ListResourceTemplatesHandler($referenceProvider, $paginationLimit),
|
|
||||||
new Handler\Request\CallToolHandler($toolCaller, $logger),
|
|
||||||
new Handler\Request\ListToolsHandler($referenceProvider, $paginationLimit),
|
|
||||||
new Handler\Request\CompletionCompleteHandler(new Completer($referenceProvider)),
|
|
||||||
],
|
|
||||||
logger: $logger,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,16 @@
|
||||||
|
|
||||||
namespace Mcp\Server\Handler\Request;
|
namespace Mcp\Server\Handler\Request;
|
||||||
|
|
||||||
use Mcp\Capability\Tool\ToolCallerInterface;
|
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
||||||
|
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||||
use Mcp\Exception\ExceptionInterface;
|
use Mcp\Exception\ExceptionInterface;
|
||||||
|
use Mcp\Exception\ToolCallException;
|
||||||
|
use Mcp\Exception\ToolNotFoundException;
|
||||||
use Mcp\Schema\JsonRpc\Error;
|
use Mcp\Schema\JsonRpc\Error;
|
||||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
use Mcp\Schema\Request\CallToolRequest;
|
use Mcp\Schema\Request\CallToolRequest;
|
||||||
|
use Mcp\Schema\Result\CallToolResult;
|
||||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||||
use Mcp\Server\Session\SessionInterface;
|
use Mcp\Server\Session\SessionInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
@ -29,7 +33,8 @@ use Psr\Log\NullLogger;
|
||||||
final class CallToolHandler implements MethodHandlerInterface
|
final class CallToolHandler implements MethodHandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ToolCallerInterface $toolCaller,
|
private readonly ReferenceProviderInterface $referenceProvider,
|
||||||
|
private readonly ReferenceHandlerInterface $referenceHandler,
|
||||||
private readonly LoggerInterface $logger = new NullLogger(),
|
private readonly LoggerInterface $logger = new NullLogger(),
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
@ -43,20 +48,44 @@ final class CallToolHandler implements MethodHandlerInterface
|
||||||
{
|
{
|
||||||
\assert($message instanceof CallToolRequest);
|
\assert($message instanceof CallToolRequest);
|
||||||
|
|
||||||
|
$toolName = $message->name;
|
||||||
|
$arguments = $message->arguments ?? [];
|
||||||
|
|
||||||
|
$this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$content = $this->toolCaller->call($message);
|
$reference = $this->referenceProvider->getTool($toolName);
|
||||||
} catch (ExceptionInterface $exception) {
|
if (null === $reference) {
|
||||||
$this->logger->error(
|
throw new ToolNotFoundException($message);
|
||||||
\sprintf('Error while executing tool "%s": "%s".', $message->name, $exception->getMessage()),
|
}
|
||||||
[
|
|
||||||
'tool' => $message->name,
|
$result = $this->referenceHandler->handle($reference, $arguments);
|
||||||
'arguments' => $message->arguments,
|
$formatted = $reference->formatResult($result);
|
||||||
],
|
|
||||||
);
|
$this->logger->debug('Tool executed successfully', [
|
||||||
|
'name' => $toolName,
|
||||||
|
'result_type' => \gettype($result),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new Response($message->getId(), new CallToolResult($formatted));
|
||||||
|
} catch (ToolNotFoundException $e) {
|
||||||
|
$this->logger->error('Tool not found', ['name' => $toolName]);
|
||||||
|
|
||||||
|
return new Error($message->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
|
||||||
|
} catch (ToolCallException|ExceptionInterface $e) {
|
||||||
|
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [
|
||||||
|
'tool' => $toolName,
|
||||||
|
'arguments' => $arguments,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Error::forInternalError('Error while executing tool', $message->getId());
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logger->error('Unhandled error during tool execution', [
|
||||||
|
'name' => $toolName,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
return Error::forInternalError('Error while executing tool', $message->getId());
|
return Error::forInternalError('Error while executing tool', $message->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response($message->getId(), $content);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,16 @@
|
||||||
|
|
||||||
namespace Mcp\Server\Handler\Request;
|
namespace Mcp\Server\Handler\Request;
|
||||||
|
|
||||||
use Mcp\Capability\Completion\CompleterInterface;
|
use Mcp\Capability\Completion\ProviderInterface;
|
||||||
use Mcp\Exception\ExceptionInterface;
|
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||||
use Mcp\Schema\JsonRpc\Error;
|
use Mcp\Schema\JsonRpc\Error;
|
||||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
use Mcp\Schema\Request\CompletionCompleteRequest;
|
use Mcp\Schema\Request\CompletionCompleteRequest;
|
||||||
|
use Mcp\Schema\Result\CompletionCompleteResult;
|
||||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||||
use Mcp\Server\Session\SessionInterface;
|
use Mcp\Server\Session\SessionInterface;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles completion/complete requests.
|
* Handles completion/complete requests.
|
||||||
|
|
@ -28,7 +30,8 @@ use Mcp\Server\Session\SessionInterface;
|
||||||
final class CompletionCompleteHandler implements MethodHandlerInterface
|
final class CompletionCompleteHandler implements MethodHandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CompleterInterface $completer,
|
private readonly ReferenceProviderInterface $referenceProvider,
|
||||||
|
private readonly ?ContainerInterface $container = null,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,12 +44,45 @@ final class CompletionCompleteHandler implements MethodHandlerInterface
|
||||||
{
|
{
|
||||||
\assert($message instanceof CompletionCompleteRequest);
|
\assert($message instanceof CompletionCompleteRequest);
|
||||||
|
|
||||||
try {
|
$name = $message->argument['name'] ?? '';
|
||||||
$result = $this->completer->complete($message);
|
$value = $message->argument['value'] ?? '';
|
||||||
} catch (ExceptionInterface) {
|
|
||||||
return Error::forInternalError('Error while handling completion request', $message->getId());
|
$reference = match ($message->ref->type) {
|
||||||
|
'ref/prompt' => $this->referenceProvider->getPrompt($message->ref->name),
|
||||||
|
'ref/resource' => $this->referenceProvider->getResourceTemplate($message->ref->uri),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (null === $reference) {
|
||||||
|
return new Response($message->getId(), new CompletionCompleteResult([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response($message->getId(), $result);
|
$providers = $reference->completionProviders;
|
||||||
|
$provider = $providers[$name] ?? null;
|
||||||
|
if (null === $provider) {
|
||||||
|
return new Response($message->getId(), new CompletionCompleteResult([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_string($provider)) {
|
||||||
|
if (!class_exists($provider)) {
|
||||||
|
return Error::forInternalError('Invalid completion provider', $message->getId());
|
||||||
|
}
|
||||||
|
$provider = $this->container?->has($provider) ? $this->container->get($provider) : new $provider();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$provider instanceof ProviderInterface) {
|
||||||
|
return Error::forInternalError('Invalid completion provider type', $message->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$completions = $provider->getCompletions($value);
|
||||||
|
$total = \count($completions);
|
||||||
|
$hasMore = $total > 100;
|
||||||
|
$paged = \array_slice($completions, 0, 100);
|
||||||
|
|
||||||
|
return new Response($message->getId(), new CompletionCompleteResult($paged, $total, $hasMore));
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return Error::forInternalError('Error while handling completion request', $message->getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,20 @@
|
||||||
|
|
||||||
namespace Mcp\Server\Handler\Request;
|
namespace Mcp\Server\Handler\Request;
|
||||||
|
|
||||||
use Mcp\Capability\Prompt\PromptGetterInterface;
|
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
||||||
|
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||||
use Mcp\Exception\ExceptionInterface;
|
use Mcp\Exception\ExceptionInterface;
|
||||||
|
use Mcp\Exception\PromptGetException;
|
||||||
|
use Mcp\Exception\PromptNotFoundException;
|
||||||
use Mcp\Schema\JsonRpc\Error;
|
use Mcp\Schema\JsonRpc\Error;
|
||||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
use Mcp\Schema\Request\GetPromptRequest;
|
use Mcp\Schema\Request\GetPromptRequest;
|
||||||
|
use Mcp\Schema\Result\GetPromptResult;
|
||||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||||
use Mcp\Server\Session\SessionInterface;
|
use Mcp\Server\Session\SessionInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||||
|
|
@ -26,7 +32,9 @@ use Mcp\Server\Session\SessionInterface;
|
||||||
final class GetPromptHandler implements MethodHandlerInterface
|
final class GetPromptHandler implements MethodHandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PromptGetterInterface $promptGetter,
|
private readonly ReferenceProviderInterface $referenceProvider,
|
||||||
|
private readonly ReferenceHandlerInterface $referenceHandler,
|
||||||
|
private readonly LoggerInterface $logger = new NullLogger(),
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,12 +47,32 @@ final class GetPromptHandler implements MethodHandlerInterface
|
||||||
{
|
{
|
||||||
\assert($message instanceof GetPromptRequest);
|
\assert($message instanceof GetPromptRequest);
|
||||||
|
|
||||||
|
$promptName = $message->name;
|
||||||
|
$arguments = $message->arguments ?? [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$messages = $this->promptGetter->get($message);
|
$reference = $this->referenceProvider->getPrompt($promptName);
|
||||||
} catch (ExceptionInterface) {
|
if (null === $reference) {
|
||||||
|
throw new PromptNotFoundException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->referenceHandler->handle($reference, $arguments);
|
||||||
|
|
||||||
|
$formatted = $reference->formatResult($result);
|
||||||
|
|
||||||
|
return new Response($message->getId(), new GetPromptResult($formatted));
|
||||||
|
} catch (PromptNotFoundException $e) {
|
||||||
|
$this->logger->error('Prompt not found', ['prompt_name' => $promptName]);
|
||||||
|
|
||||||
|
return new Error($message->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
|
||||||
|
} catch (PromptGetException|ExceptionInterface $e) {
|
||||||
|
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
|
||||||
|
|
||||||
|
return Error::forInternalError('Error while handling prompt', $message->getId());
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
|
||||||
|
|
||||||
return Error::forInternalError('Error while handling prompt', $message->getId());
|
return Error::forInternalError('Error while handling prompt', $message->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response($message->getId(), $messages);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,19 @@
|
||||||
|
|
||||||
namespace Mcp\Server\Handler\Request;
|
namespace Mcp\Server\Handler\Request;
|
||||||
|
|
||||||
use Mcp\Capability\Resource\ResourceReaderInterface;
|
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
||||||
use Mcp\Exception\ExceptionInterface;
|
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||||
|
use Mcp\Capability\Registry\ResourceTemplateReference;
|
||||||
use Mcp\Exception\ResourceNotFoundException;
|
use Mcp\Exception\ResourceNotFoundException;
|
||||||
use Mcp\Schema\JsonRpc\Error;
|
use Mcp\Schema\JsonRpc\Error;
|
||||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
use Mcp\Schema\Request\ReadResourceRequest;
|
use Mcp\Schema\Request\ReadResourceRequest;
|
||||||
|
use Mcp\Schema\Result\ReadResourceResult;
|
||||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||||
use Mcp\Server\Session\SessionInterface;
|
use Mcp\Server\Session\SessionInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||||
|
|
@ -27,7 +31,9 @@ use Mcp\Server\Session\SessionInterface;
|
||||||
final class ReadResourceHandler implements MethodHandlerInterface
|
final class ReadResourceHandler implements MethodHandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ResourceReaderInterface $resourceReader,
|
private readonly ReferenceProviderInterface $referenceProvider,
|
||||||
|
private readonly ReferenceHandlerInterface $referenceHandler,
|
||||||
|
private readonly LoggerInterface $logger = new NullLogger(),
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,14 +46,33 @@ final class ReadResourceHandler implements MethodHandlerInterface
|
||||||
{
|
{
|
||||||
\assert($message instanceof ReadResourceRequest);
|
\assert($message instanceof ReadResourceRequest);
|
||||||
|
|
||||||
|
$uri = $message->uri;
|
||||||
|
|
||||||
|
$this->logger->debug('Reading resource', ['uri' => $uri]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$contents = $this->resourceReader->read($message);
|
$reference = $this->referenceProvider->getResource($uri);
|
||||||
|
if (null === $reference) {
|
||||||
|
throw new ResourceNotFoundException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->referenceHandler->handle($reference, ['uri' => $uri]);
|
||||||
|
|
||||||
|
if ($reference instanceof ResourceTemplateReference) {
|
||||||
|
$formatted = $reference->formatResult($result, $uri, $reference->resourceTemplate->mimeType);
|
||||||
|
} else {
|
||||||
|
$formatted = $reference->formatResult($result, $uri, $reference->schema->mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response($message->getId(), new ReadResourceResult($formatted));
|
||||||
} catch (ResourceNotFoundException $e) {
|
} catch (ResourceNotFoundException $e) {
|
||||||
|
$this->logger->error('Resource not found', ['uri' => $uri]);
|
||||||
|
|
||||||
return new Error($message->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
|
return new Error($message->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
|
||||||
} catch (ExceptionInterface) {
|
} catch (\Throwable $e) {
|
||||||
|
$this->logger->error(\sprintf('Error while reading resource "%s": "%s".', $uri, $e->getMessage()));
|
||||||
|
|
||||||
return Error::forInternalError('Error while reading resource', $message->getId());
|
return Error::forInternalError('Error while reading resource', $message->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response($message->getId(), $contents);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Tests\Unit\Capability\Prompt\Completion;
|
|
||||||
|
|
||||||
use Mcp\Capability\Completion\EnumCompletionProvider;
|
|
||||||
use Mcp\Exception\InvalidArgumentException;
|
|
||||||
use Mcp\Tests\Unit\Fixtures\Enum\PriorityEnum;
|
|
||||||
use Mcp\Tests\Unit\Fixtures\Enum\StatusEnum;
|
|
||||||
use Mcp\Tests\Unit\Fixtures\Enum\UnitEnum;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class EnumCompletionProviderTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testCreatesProviderFromStringBackedEnum()
|
|
||||||
{
|
|
||||||
$provider = new EnumCompletionProvider(StatusEnum::class);
|
|
||||||
$result = $provider->getCompletions('');
|
|
||||||
$this->assertSame(['draft', 'published', 'archived'], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreatesProviderFromIntBackedEnumUsingNames()
|
|
||||||
{
|
|
||||||
$provider = new EnumCompletionProvider(PriorityEnum::class);
|
|
||||||
$result = $provider->getCompletions('');
|
|
||||||
|
|
||||||
$this->assertSame(['LOW', 'MEDIUM', 'HIGH'], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreatesProviderFromUnitEnumUsingNames()
|
|
||||||
{
|
|
||||||
$provider = new EnumCompletionProvider(UnitEnum::class);
|
|
||||||
$result = $provider->getCompletions('');
|
|
||||||
|
|
||||||
$this->assertSame(['Yes', 'No'], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFiltersStringEnumValuesByPrefix()
|
|
||||||
{
|
|
||||||
$provider = new EnumCompletionProvider(StatusEnum::class);
|
|
||||||
$result = $provider->getCompletions('ar');
|
|
||||||
|
|
||||||
$this->assertEquals(['archived'], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFiltersUnitEnumValuesByPrefix()
|
|
||||||
{
|
|
||||||
$provider = new EnumCompletionProvider(UnitEnum::class);
|
|
||||||
$result = $provider->getCompletions('Y');
|
|
||||||
|
|
||||||
$this->assertSame(['Yes'], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReturnsEmptyArrayWhenNoValuesMatchPrefix()
|
|
||||||
{
|
|
||||||
$provider = new EnumCompletionProvider(StatusEnum::class);
|
|
||||||
$result = $provider->getCompletions('xyz');
|
|
||||||
|
|
||||||
$this->assertSame([], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsExceptionForNonEnumClass()
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
$this->expectExceptionMessage('Class "stdClass" is not an enum.');
|
|
||||||
|
|
||||||
new EnumCompletionProvider(\stdClass::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsExceptionForNonExistentClass()
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidArgumentException::class);
|
|
||||||
$this->expectExceptionMessage('Class "NonExistentClass" is not an enum.');
|
|
||||||
|
|
||||||
new EnumCompletionProvider('NonExistentClass'); /* @phpstan-ignore argument.type */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Tests\Unit\Capability\Prompt\Completion;
|
|
||||||
|
|
||||||
use Mcp\Capability\Completion\ListCompletionProvider;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class ListCompletionProviderTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testReturnsAllValuesWhenCurrentValueIsEmpty()
|
|
||||||
{
|
|
||||||
$values = ['apple', 'banana', 'cherry'];
|
|
||||||
$provider = new ListCompletionProvider($values);
|
|
||||||
$result = $provider->getCompletions('');
|
|
||||||
|
|
||||||
$this->assertSame($values, $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFiltersValuesBasedOnCurrentValuePrefix()
|
|
||||||
{
|
|
||||||
$values = ['apple', 'apricot', 'banana', 'cherry'];
|
|
||||||
$provider = new ListCompletionProvider($values);
|
|
||||||
$result = $provider->getCompletions('ap');
|
|
||||||
|
|
||||||
$this->assertSame(['apple', 'apricot'], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReturnsEmptyArrayWhenNoValuesMatch()
|
|
||||||
{
|
|
||||||
$values = ['apple', 'banana', 'cherry'];
|
|
||||||
$provider = new ListCompletionProvider($values);
|
|
||||||
$result = $provider->getCompletions('xyz');
|
|
||||||
|
|
||||||
$this->assertSame([], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWorksWithSingleCharacterPrefix()
|
|
||||||
{
|
|
||||||
$values = ['apple', 'banana', 'cherry'];
|
|
||||||
$provider = new ListCompletionProvider($values);
|
|
||||||
$result = $provider->getCompletions('a');
|
|
||||||
|
|
||||||
$this->assertSame(['apple'], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testIsCaseSensitiveByDefault()
|
|
||||||
{
|
|
||||||
$values = ['Apple', 'apple', 'APPLE'];
|
|
||||||
$provider = new ListCompletionProvider($values);
|
|
||||||
$result = $provider->getCompletions('A');
|
|
||||||
|
|
||||||
$this->assertEquals(['Apple', 'APPLE'], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testHandlesEmptyValuesArray()
|
|
||||||
{
|
|
||||||
$provider = new ListCompletionProvider([]);
|
|
||||||
$result = $provider->getCompletions('test');
|
|
||||||
|
|
||||||
$this->assertSame([], $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPreservesArrayOrder()
|
|
||||||
{
|
|
||||||
$values = ['zebra', 'apple', 'banana'];
|
|
||||||
$provider = new ListCompletionProvider($values);
|
|
||||||
$result = $provider->getCompletions('');
|
|
||||||
|
|
||||||
$this->assertSame(['zebra', 'apple', 'banana'], $result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,638 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the official PHP MCP SDK.
|
|
||||||
*
|
|
||||||
* A collaboration between Symfony and the PHP Foundation.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Mcp\Tests\Unit\Capability\Prompt;
|
|
||||||
|
|
||||||
use Mcp\Capability\Completion\EnumCompletionProvider;
|
|
||||||
use Mcp\Capability\Prompt\PromptGetter;
|
|
||||||
use Mcp\Capability\Registry\PromptReference;
|
|
||||||
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
|
||||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|
||||||
use Mcp\Exception\PromptGetException;
|
|
||||||
use Mcp\Exception\PromptNotFoundException;
|
|
||||||
use Mcp\Exception\RegistryException;
|
|
||||||
use Mcp\Schema\Content\PromptMessage;
|
|
||||||
use Mcp\Schema\Content\TextContent;
|
|
||||||
use Mcp\Schema\Enum\Role;
|
|
||||||
use Mcp\Schema\Prompt;
|
|
||||||
use Mcp\Schema\Request\GetPromptRequest;
|
|
||||||
use Mcp\Schema\Result\GetPromptResult;
|
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
|
|
||||||
class PromptGetterTest extends TestCase
|
|
||||||
{
|
|
||||||
private PromptGetter $promptGetter;
|
|
||||||
private ReferenceProviderInterface|MockObject $referenceProvider;
|
|
||||||
private ReferenceHandlerInterface|MockObject $referenceHandler;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
$this->referenceProvider = $this->createMock(ReferenceProviderInterface::class);
|
|
||||||
$this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class);
|
|
||||||
|
|
||||||
$this->promptGetter = new PromptGetter(
|
|
||||||
$this->referenceProvider,
|
|
||||||
$this->referenceHandler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetExecutesPromptSuccessfully(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('test_prompt', ['param' => 'value']);
|
|
||||||
$prompt = $this->createValidPrompt('test_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'test result');
|
|
||||||
$handlerResult = [
|
|
||||||
'role' => 'user',
|
|
||||||
'content' => 'Generated prompt content',
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('test_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, ['param' => 'value'])
|
|
||||||
->willReturn($handlerResult);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(1, $result->messages);
|
|
||||||
$this->assertInstanceOf(PromptMessage::class, $result->messages[0]);
|
|
||||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
||||||
$this->assertInstanceOf(TextContent::class, $result->messages[0]->content);
|
|
||||||
$this->assertEquals('Generated prompt content', $result->messages[0]->content->text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetWithEmptyArguments(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('empty_args_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('empty_args_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'Empty args content');
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('empty_args_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn([
|
|
||||||
'role' => 'user',
|
|
||||||
'content' => 'Empty args content',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(1, $result->messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetWithComplexArguments(): void
|
|
||||||
{
|
|
||||||
$arguments = [
|
|
||||||
'string_param' => 'value',
|
|
||||||
'int_param' => 42,
|
|
||||||
'bool_param' => true,
|
|
||||||
'array_param' => ['nested' => 'data'],
|
|
||||||
'null_param' => null,
|
|
||||||
];
|
|
||||||
$request = new GetPromptRequest('complex_prompt', $arguments);
|
|
||||||
$prompt = $this->createValidPrompt('complex_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'Complex content');
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('complex_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, $arguments)
|
|
||||||
->willReturn([
|
|
||||||
'role' => 'assistant',
|
|
||||||
'content' => 'Complex response',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(1, $result->messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetThrowsInvalidArgumentExceptionWhenPromptNotFound(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('nonexistent_prompt', ['param' => 'value']);
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('nonexistent_prompt')
|
|
||||||
->willReturn(null);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->never())
|
|
||||||
->method('handle');
|
|
||||||
|
|
||||||
$this->expectException(PromptNotFoundException::class);
|
|
||||||
$this->expectExceptionMessage('Prompt not found for name: "nonexistent_prompt".');
|
|
||||||
|
|
||||||
$this->promptGetter->get($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetThrowsRegistryExceptionWhenHandlerFails(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('failing_prompt', ['param' => 'value']);
|
|
||||||
$prompt = $this->createValidPrompt('failing_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => throw new \RuntimeException('Handler failed'));
|
|
||||||
$handlerException = RegistryException::internalError('Handler failed');
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('failing_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, ['param' => 'value'])
|
|
||||||
->willThrowException($handlerException);
|
|
||||||
|
|
||||||
$this->expectException(PromptGetException::class);
|
|
||||||
|
|
||||||
$this->promptGetter->get($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetHandlesJsonExceptionDuringFormatting(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('json_error_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('json_error_prompt');
|
|
||||||
|
|
||||||
// Create a mock PromptReference that will throw JsonException during formatResult
|
|
||||||
$promptReference = $this->createMock(PromptReference::class);
|
|
||||||
$promptReference->expects($this->once())
|
|
||||||
->method('formatResult')
|
|
||||||
->willThrowException(new \JsonException('JSON encoding failed'));
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('json_error_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn('some result');
|
|
||||||
|
|
||||||
$this->expectException(PromptGetException::class);
|
|
||||||
$this->expectExceptionMessage('JSON encoding failed');
|
|
||||||
|
|
||||||
$this->promptGetter->get($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetHandlesArrayOfMessages(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('multi_message_prompt', ['context' => 'test']);
|
|
||||||
$prompt = $this->createValidPrompt('multi_message_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'Multiple messages');
|
|
||||||
$handlerResult = [
|
|
||||||
[
|
|
||||||
'role' => 'user',
|
|
||||||
'content' => 'First message',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'role' => 'assistant',
|
|
||||||
'content' => 'Second message',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('multi_message_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, ['context' => 'test'])
|
|
||||||
->willReturn($handlerResult);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(2, $result->messages);
|
|
||||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
||||||
$this->assertEquals('First message', $result->messages[0]->content->text);
|
|
||||||
$this->assertEquals(Role::Assistant, $result->messages[1]->role);
|
|
||||||
$this->assertEquals('Second message', $result->messages[1]->content->text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetHandlesPromptMessageObjects(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('prompt_message_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('prompt_message_prompt');
|
|
||||||
$promptMessage = new PromptMessage(
|
|
||||||
Role::User,
|
|
||||||
new TextContent('Direct prompt message')
|
|
||||||
);
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => $promptMessage);
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('prompt_message_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn($promptMessage);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(1, $result->messages);
|
|
||||||
$this->assertSame($promptMessage, $result->messages[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetHandlesUserAssistantStructure(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('user_assistant_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('user_assistant_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'Conversation content');
|
|
||||||
$handlerResult = [
|
|
||||||
'user' => 'What is the weather?',
|
|
||||||
'assistant' => 'I can help you check the weather.',
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('user_assistant_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn($handlerResult);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(2, $result->messages);
|
|
||||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
||||||
$this->assertEquals('What is the weather?', $result->messages[0]->content->text);
|
|
||||||
$this->assertEquals(Role::Assistant, $result->messages[1]->role);
|
|
||||||
$this->assertEquals('I can help you check the weather.', $result->messages[1]->content->text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetHandlesEmptyArrayResult(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('empty_array_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('empty_array_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => []);
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('empty_array_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn([]);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(0, $result->messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetWithTypedContentStructure(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('typed_content_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('typed_content_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'Typed content');
|
|
||||||
$handlerResult = [
|
|
||||||
'role' => 'user',
|
|
||||||
'content' => [
|
|
||||||
'type' => 'text',
|
|
||||||
'text' => 'Typed text content',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('typed_content_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn($handlerResult);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(1, $result->messages);
|
|
||||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
||||||
$this->assertEquals('Typed text content', $result->messages[0]->content->text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetWithPromptReferenceHavingCompletionProviders(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('completion_prompt', ['param' => 'value']);
|
|
||||||
$prompt = $this->createValidPrompt('completion_prompt');
|
|
||||||
$completionProviders = ['param' => EnumCompletionProvider::class];
|
|
||||||
$promptReference = new PromptReference(
|
|
||||||
$prompt,
|
|
||||||
fn () => 'Completion content',
|
|
||||||
false,
|
|
||||||
$completionProviders
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('completion_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, ['param' => 'value'])
|
|
||||||
->willReturn([
|
|
||||||
'role' => 'user',
|
|
||||||
'content' => 'Completion content',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(1, $result->messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetHandlesMixedMessageArray(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('mixed_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('mixed_prompt');
|
|
||||||
$promptMessage = new PromptMessage(Role::Assistant, new TextContent('Direct message'));
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'Mixed content');
|
|
||||||
$handlerResult = [
|
|
||||||
$promptMessage,
|
|
||||||
[
|
|
||||||
'role' => 'user',
|
|
||||||
'content' => 'Regular message',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('mixed_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn($handlerResult);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(2, $result->messages);
|
|
||||||
$this->assertSame($promptMessage, $result->messages[0]);
|
|
||||||
$this->assertEquals(Role::User, $result->messages[1]->role);
|
|
||||||
$this->assertEquals('Regular message', $result->messages[1]->content->text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetReflectsFormattedMessagesCorrectly(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('format_test_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('format_test_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'Format test');
|
|
||||||
|
|
||||||
// Test that the formatted result from PromptReference.formatResult is properly returned
|
|
||||||
$handlerResult = [
|
|
||||||
'role' => 'user',
|
|
||||||
'content' => 'Test formatting',
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('format_test_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn($handlerResult);
|
|
||||||
|
|
||||||
$result = $this->promptGetter->get($request);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
|
||||||
$this->assertCount(1, $result->messages);
|
|
||||||
$this->assertEquals('Test formatting', $result->messages[0]->content->text);
|
|
||||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that invalid handler results throw RuntimeException from PromptReference.formatResult().
|
|
||||||
*/
|
|
||||||
public function testGetThrowsRuntimeExceptionForInvalidHandlerResult(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('invalid_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('invalid_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 'Invalid content');
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('invalid_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn('This is not a valid prompt format');
|
|
||||||
|
|
||||||
$this->expectException(PromptGetException::class);
|
|
||||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
||||||
|
|
||||||
$this->promptGetter->get($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that null result from handler throws RuntimeException.
|
|
||||||
*/
|
|
||||||
public function testGetThrowsRuntimeExceptionForNullHandlerResult(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('null_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('null_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => null);
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('null_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn(null);
|
|
||||||
|
|
||||||
$this->expectException(PromptGetException::class);
|
|
||||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
||||||
|
|
||||||
$this->promptGetter->get($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that scalar result from handler throws RuntimeException.
|
|
||||||
*/
|
|
||||||
public function testGetThrowsRuntimeExceptionForScalarHandlerResult(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('scalar_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('scalar_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => 42);
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('scalar_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn(42);
|
|
||||||
|
|
||||||
$this->expectException(PromptGetException::class);
|
|
||||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
||||||
|
|
||||||
$this->promptGetter->get($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that boolean result from handler throws RuntimeException.
|
|
||||||
*/
|
|
||||||
public function testGetThrowsRuntimeExceptionForBooleanHandlerResult(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('boolean_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('boolean_prompt');
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => true);
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('boolean_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn(true);
|
|
||||||
|
|
||||||
$this->expectException(PromptGetException::class);
|
|
||||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
||||||
|
|
||||||
$this->promptGetter->get($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that object result from handler throws RuntimeException.
|
|
||||||
*/
|
|
||||||
public function testGetThrowsRuntimeExceptionForObjectHandlerResult(): void
|
|
||||||
{
|
|
||||||
$request = new GetPromptRequest('object_prompt', []);
|
|
||||||
$prompt = $this->createValidPrompt('object_prompt');
|
|
||||||
$objectResult = new \stdClass();
|
|
||||||
$objectResult->property = 'value';
|
|
||||||
$promptReference = new PromptReference($prompt, fn () => $objectResult);
|
|
||||||
|
|
||||||
$this->referenceProvider
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getPrompt')
|
|
||||||
->with('object_prompt')
|
|
||||||
->willReturn($promptReference);
|
|
||||||
|
|
||||||
$this->referenceHandler
|
|
||||||
->expects($this->once())
|
|
||||||
->method('handle')
|
|
||||||
->with($promptReference, [])
|
|
||||||
->willReturn($objectResult);
|
|
||||||
|
|
||||||
$this->expectException(PromptGetException::class);
|
|
||||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
|
||||||
|
|
||||||
$this->promptGetter->get($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testConstructorWithDefaultLogger(): void
|
|
||||||
{
|
|
||||||
$promptGetter = new PromptGetter(
|
|
||||||
$this->referenceProvider,
|
|
||||||
$this->referenceHandler,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(PromptGetter::class, $promptGetter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testConstructorWithCustomLogger(): void
|
|
||||||
{
|
|
||||||
$logger = $this->createMock(LoggerInterface::class);
|
|
||||||
$promptGetter = new PromptGetter(
|
|
||||||
$this->referenceProvider,
|
|
||||||
$this->referenceHandler,
|
|
||||||
$logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(PromptGetter::class, $promptGetter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createValidPrompt(string $name): Prompt
|
|
||||||
{
|
|
||||||
return new Prompt(
|
|
||||||
name: $name,
|
|
||||||
description: "Test prompt: {$name}",
|
|
||||||
arguments: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue