Adopt server examples
This commit is contained in:
parent
da0d5a87a0
commit
2ef051a355
8 changed files with 3639 additions and 756 deletions
|
|
@ -11,13 +11,16 @@
|
|||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Tool\ToolExecutorInterface;
|
||||
use Mcp\Capability\Registry;
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\CallToolRequest;
|
||||
use Mcp\Schema\Result\CallToolResult;
|
||||
use Mcp\Server\MethodHandlerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
|
|
@ -26,7 +29,8 @@ use Mcp\Server\MethodHandlerInterface;
|
|||
final class CallToolHandler implements MethodHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ToolExecutorInterface $toolExecutor,
|
||||
private readonly Registry $registry,
|
||||
private readonly LoggerInterface $logger = new NullLogger(),
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -40,11 +44,16 @@ final class CallToolHandler implements MethodHandlerInterface
|
|||
\assert($message instanceof CallToolRequest);
|
||||
|
||||
try {
|
||||
$result = $this->toolExecutor->call($message);
|
||||
} catch (ExceptionInterface) {
|
||||
$content = $this->registry->handleCallTool($message->name, $message->arguments);
|
||||
} catch (ExceptionInterface $exception) {
|
||||
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $message->name, $exception->getMessage()), [
|
||||
'tool' => $message->name,
|
||||
'arguments' => $message->arguments,
|
||||
]);
|
||||
|
||||
return Error::forInternalError('Error while executing tool', $message->getId());
|
||||
}
|
||||
|
||||
return new Response($message->getId(), $result);
|
||||
return new Response($message->getId(), new CallToolResult($content));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@
|
|||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Prompt\PromptGetterInterface;
|
||||
use Mcp\Capability\Registry;
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\GetPromptRequest;
|
||||
use Mcp\Schema\Result\GetPromptResult;
|
||||
use Mcp\Server\MethodHandlerInterface;
|
||||
|
||||
/**
|
||||
|
|
@ -25,7 +26,7 @@ use Mcp\Server\MethodHandlerInterface;
|
|||
final class GetPromptHandler implements MethodHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PromptGetterInterface $getter,
|
||||
private readonly Registry $registry,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -39,11 +40,11 @@ final class GetPromptHandler implements MethodHandlerInterface
|
|||
\assert($message instanceof GetPromptRequest);
|
||||
|
||||
try {
|
||||
$result = $this->getter->get($message);
|
||||
$messages = $this->registry->handleGetPrompt($message->name, $message->arguments);
|
||||
} catch (ExceptionInterface) {
|
||||
return Error::forInternalError('Error while handling prompt', $message->getId());
|
||||
}
|
||||
|
||||
return new Response($message->getId(), $result);
|
||||
return new Response($message->getId(), new GetPromptResult($messages));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,9 @@
|
|||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Prompt\CollectionInterface;
|
||||
use Mcp\Capability\Registry;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Prompt;
|
||||
use Mcp\Schema\PromptArgument;
|
||||
use Mcp\Schema\Request\ListPromptsRequest;
|
||||
use Mcp\Schema\Result\ListPromptsResult;
|
||||
use Mcp\Server\MethodHandlerInterface;
|
||||
|
|
@ -26,7 +24,7 @@ use Mcp\Server\MethodHandlerInterface;
|
|||
final class ListPromptsHandler implements MethodHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionInterface $collection,
|
||||
private readonly Registry $registry,
|
||||
private readonly int $pageSize = 20,
|
||||
) {
|
||||
}
|
||||
|
|
@ -41,19 +39,7 @@ final class ListPromptsHandler implements MethodHandlerInterface
|
|||
\assert($message instanceof ListPromptsRequest);
|
||||
|
||||
$cursor = null;
|
||||
$prompts = [];
|
||||
|
||||
$metadataList = $this->collection->getMetadata($this->pageSize, $message->cursor);
|
||||
|
||||
foreach ($metadataList as $metadata) {
|
||||
$cursor = $metadata->getName();
|
||||
$prompts[] = new Prompt(
|
||||
$metadata->getName(),
|
||||
$metadata->getDescription(),
|
||||
array_map(fn (array $data) => PromptArgument::fromArray($data), $metadata->getArguments()),
|
||||
);
|
||||
}
|
||||
|
||||
$prompts = $this->registry->getPrompts($this->pageSize, $message->cursor);
|
||||
$nextCursor = (null !== $cursor && \count($prompts) === $this->pageSize) ? $cursor : null;
|
||||
|
||||
return new Response(
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@
|
|||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Resource\CollectionInterface;
|
||||
use Mcp\Capability\Registry;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ListResourcesRequest;
|
||||
use Mcp\Schema\Resource;
|
||||
use Mcp\Schema\Result\ListResourcesResult;
|
||||
use Mcp\Server\MethodHandlerInterface;
|
||||
|
||||
|
|
@ -25,7 +24,7 @@ use Mcp\Server\MethodHandlerInterface;
|
|||
final class ListResourcesHandler implements MethodHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionInterface $collection,
|
||||
private readonly Registry $registry,
|
||||
private readonly int $pageSize = 20,
|
||||
) {
|
||||
}
|
||||
|
|
@ -40,22 +39,7 @@ final class ListResourcesHandler implements MethodHandlerInterface
|
|||
\assert($message instanceof ListResourcesRequest);
|
||||
|
||||
$cursor = null;
|
||||
$resources = [];
|
||||
|
||||
$metadataList = $this->collection->getMetadata($this->pageSize, $message->cursor);
|
||||
|
||||
foreach ($metadataList as $metadata) {
|
||||
$cursor = $metadata->getUri();
|
||||
$resources[] = new Resource(
|
||||
$metadata->getUri(),
|
||||
$metadata->getName(),
|
||||
$metadata->getDescription(),
|
||||
$metadata->getMimeType(),
|
||||
null,
|
||||
$metadata->getSize(),
|
||||
);
|
||||
}
|
||||
|
||||
$resources = $this->registry->getResources($this->pageSize, $message->cursor);
|
||||
$nextCursor = (null !== $cursor && \count($resources) === $this->pageSize) ? $cursor : null;
|
||||
|
||||
return new Response(
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@
|
|||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Tool\CollectionInterface;
|
||||
use Mcp\Capability\Registry;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ListToolsRequest;
|
||||
use Mcp\Schema\Result\ListToolsResult;
|
||||
use Mcp\Schema\Tool;
|
||||
use Mcp\Server\MethodHandlerInterface;
|
||||
|
||||
/**
|
||||
|
|
@ -26,7 +25,7 @@ use Mcp\Server\MethodHandlerInterface;
|
|||
final class ListToolsHandler implements MethodHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionInterface $collection,
|
||||
private readonly Registry $registry,
|
||||
private readonly int $pageSize = 20,
|
||||
) {
|
||||
}
|
||||
|
|
@ -41,24 +40,7 @@ final class ListToolsHandler implements MethodHandlerInterface
|
|||
\assert($message instanceof ListToolsRequest);
|
||||
|
||||
$cursor = null;
|
||||
$tools = [];
|
||||
|
||||
$metadataList = $this->collection->getMetadata($this->pageSize, $message->cursor);
|
||||
|
||||
foreach ($metadataList as $tool) {
|
||||
$cursor = $tool->getName();
|
||||
$inputSchema = $tool->getInputSchema();
|
||||
$tools[] = new Tool(
|
||||
$tool->getName(),
|
||||
[] === $inputSchema ? [
|
||||
'type' => 'object',
|
||||
'$schema' => 'http://json-schema.org/draft-07/schema#',
|
||||
] : $inputSchema,
|
||||
$tool->getDescription(),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
$tools = $this->registry->getTools($this->pageSize, $message->cursor);
|
||||
$nextCursor = (null !== $cursor && \count($tools) === $this->pageSize) ? $cursor : null;
|
||||
|
||||
return new Response(
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@
|
|||
|
||||
namespace Mcp\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Resource\ResourceReaderInterface;
|
||||
use Mcp\Capability\Registry;
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Exception\ResourceNotFoundException;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ReadResourceRequest;
|
||||
use Mcp\Schema\Result\ReadResourceResult;
|
||||
use Mcp\Server\MethodHandlerInterface;
|
||||
|
||||
/**
|
||||
|
|
@ -26,7 +27,7 @@ use Mcp\Server\MethodHandlerInterface;
|
|||
final class ReadResourceHandler implements MethodHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ResourceReaderInterface $reader,
|
||||
private readonly Registry $registry,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -40,13 +41,13 @@ final class ReadResourceHandler implements MethodHandlerInterface
|
|||
\assert($message instanceof ReadResourceRequest);
|
||||
|
||||
try {
|
||||
$result = $this->reader->read($message);
|
||||
$contents = $this->registry->handleReadResource($message->uri);
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
return new Error($message->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
|
||||
} catch (ExceptionInterface) {
|
||||
return Error::forInternalError('Error while reading resource', $message->getId());
|
||||
}
|
||||
|
||||
return new Response($message->getId(), $result);
|
||||
return new Response($message->getId(), new ReadResourceResult($contents));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
418
src/Server/ServerBuilder.php
Normal file
418
src/Server/ServerBuilder.php
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
<?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\Server;
|
||||
|
||||
use Mcp\Capability\Attribute\CompletionProvider;
|
||||
use Mcp\Capability\Discovery\Discoverer;
|
||||
use Mcp\Capability\Discovery\DocBlockParser;
|
||||
use Mcp\Capability\Discovery\HandlerResolver;
|
||||
use Mcp\Capability\Discovery\SchemaGenerator;
|
||||
use Mcp\Capability\Prompt\Completion\EnumCompletionProvider;
|
||||
use Mcp\Capability\Prompt\Completion\ListCompletionProvider;
|
||||
use Mcp\Capability\Registry;
|
||||
use Mcp\Capability\Registry\Container;
|
||||
use Mcp\Capability\Registry\ReferenceHandler;
|
||||
use Mcp\JsonRpc\Handler;
|
||||
use Mcp\Schema\Annotations;
|
||||
use Mcp\Schema\Implementation;
|
||||
use Mcp\Schema\Prompt;
|
||||
use Mcp\Schema\PromptArgument;
|
||||
use Mcp\Schema\Resource;
|
||||
use Mcp\Schema\ResourceTemplate;
|
||||
use Mcp\Schema\Tool;
|
||||
use Mcp\Schema\ToolAnnotations;
|
||||
use Mcp\Server;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
/**
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
final class ServerBuilder
|
||||
{
|
||||
private ?Implementation $serverInfo = null;
|
||||
|
||||
private ?LoggerInterface $logger = null;
|
||||
|
||||
private ?CacheInterface $cache = null;
|
||||
|
||||
private ?EventDispatcherInterface $eventDispatcher = null;
|
||||
|
||||
private ?ContainerInterface $container = null;
|
||||
|
||||
private ?int $paginationLimit = 50;
|
||||
|
||||
private ?string $instructions = null;
|
||||
|
||||
/** @var array<
|
||||
* array{handler: array|string|Closure,
|
||||
* name: string|null,
|
||||
* description: string|null,
|
||||
* annotations: ToolAnnotations|null}
|
||||
* > */
|
||||
private array $manualTools = [];
|
||||
|
||||
/** @var array<
|
||||
* array{handler: array|string|Closure,
|
||||
* uri: string,
|
||||
* name: string|null,
|
||||
* description: string|null,
|
||||
* mimeType: string|null,
|
||||
* size: int|null,
|
||||
* annotations: Annotations|null}
|
||||
* > */
|
||||
private array $manualResources = [];
|
||||
|
||||
/** @var array<
|
||||
* array{handler: array|string|Closure,
|
||||
* uriTemplate: string,
|
||||
* name: string|null,
|
||||
* description: string|null,
|
||||
* mimeType: string|null,
|
||||
* annotations: Annotations|null}
|
||||
* > */
|
||||
private array $manualResourceTemplates = [];
|
||||
/** @var array<
|
||||
* array{handler: array|string|Closure,
|
||||
* name: string|null,
|
||||
* description: string|null}
|
||||
* > */
|
||||
private array $manualPrompts = [];
|
||||
private ?string $discoveryBasePath = null;
|
||||
/**
|
||||
* @var array|string[]
|
||||
*/
|
||||
private array $discoveryScanDirs = [];
|
||||
private array $discoveryExcludeDirs = [];
|
||||
|
||||
/**
|
||||
* Sets the server's identity. Required.
|
||||
*/
|
||||
public function withServerInfo(string $name, string $version, ?string $description = null): self
|
||||
{
|
||||
$this->serverInfo = new Implementation(trim($name), trim($version), $description);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the server's pagination limit.
|
||||
*/
|
||||
public function withPaginationLimit(int $paginationLimit): self
|
||||
{
|
||||
$this->paginationLimit = $paginationLimit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the instructions describing how to use the server and its features.
|
||||
*
|
||||
* This can be used by clients to improve the LLM's understanding of available tools, resources,
|
||||
* etc. It can be thought of like a "hint" to the model. For example, this information MAY
|
||||
* be added to the system prompt.
|
||||
*/
|
||||
public function withInstructions(?string $instructions): self
|
||||
{
|
||||
$this->instructions = $instructions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a PSR-3 logger instance. Defaults to NullLogger.
|
||||
*/
|
||||
public function withLogger(LoggerInterface $logger): self
|
||||
{
|
||||
$this->logger = $logger;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withEventDispatcher(EventDispatcherInterface $eventDispatcher): self
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a PSR-11 DI container, primarily for resolving user-defined handler classes.
|
||||
* Defaults to a basic internal container.
|
||||
*/
|
||||
public function withContainer(ContainerInterface $container): self
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withDiscovery(
|
||||
string $basePath,
|
||||
array $scanDirs = ['.', 'src'],
|
||||
array $excludeDirs = [],
|
||||
): self {
|
||||
$this->discoveryBasePath = $basePath;
|
||||
$this->discoveryScanDirs = $scanDirs;
|
||||
$this->discoveryExcludeDirs = $excludeDirs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually registers a tool handler.
|
||||
*/
|
||||
public function withTool(callable|array|string $handler, ?string $name = null, ?string $description = null, ?ToolAnnotations $annotations = null, ?array $inputSchema = null): self
|
||||
{
|
||||
$this->manualTools[] = compact('handler', 'name', 'description', 'annotations', 'inputSchema');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually registers a resource handler.
|
||||
*/
|
||||
public function withResource(callable|array|string $handler, string $uri, ?string $name = null, ?string $description = null, ?string $mimeType = null, ?int $size = null, ?Annotations $annotations = null): self
|
||||
{
|
||||
$this->manualResources[] = compact('handler', 'uri', 'name', 'description', 'mimeType', 'size', 'annotations');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually registers a resource template handler.
|
||||
*/
|
||||
public function withResourceTemplate(callable|array|string $handler, string $uriTemplate, ?string $name = null, ?string $description = null, ?string $mimeType = null, ?Annotations $annotations = null): self
|
||||
{
|
||||
$this->manualResourceTemplates[] = compact('handler', 'uriTemplate', 'name', 'description', 'mimeType', 'annotations');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually registers a prompt handler.
|
||||
*/
|
||||
public function withPrompt(callable|array|string $handler, ?string $name = null, ?string $description = null): self
|
||||
{
|
||||
$this->manualPrompts[] = compact('handler', 'name', 'description');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the fully configured Server instance.
|
||||
*/
|
||||
public function build(): Server
|
||||
{
|
||||
$container = $this->container ?? new Container();
|
||||
$registry = new Registry(new ReferenceHandler($container), $this->eventDispatcher, $this->logger);
|
||||
|
||||
if (null !== $this->discoveryBasePath) {
|
||||
$discovery = new Discoverer($registry, $this->logger);
|
||||
$discovery->discover($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs);
|
||||
}
|
||||
|
||||
return new Server(
|
||||
Handler::make($registry, $this->serverInfo, $this->logger),
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to perform the actual registration based on stored data.
|
||||
* Moved into the builder.
|
||||
*/
|
||||
private function registerManualElements(Registry $registry, LoggerInterface $logger): void
|
||||
{
|
||||
if (empty($this->manualTools) && empty($this->manualResources) && empty($this->manualResourceTemplates) && empty($this->manualPrompts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$docBlockParser = new DocBlockParser(logger: $logger);
|
||||
$schemaGenerator = new SchemaGenerator($docBlockParser);
|
||||
|
||||
// Register Tools
|
||||
foreach ($this->manualTools as $data) {
|
||||
try {
|
||||
$reflection = HandlerResolver::resolve($data['handler']);
|
||||
|
||||
if ($reflection instanceof \ReflectionFunction) {
|
||||
$name = $data['name'] ?? 'closure_tool_'.spl_object_id($data['handler']);
|
||||
$description = $data['description'] ?? null;
|
||||
} else {
|
||||
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
||||
$methodName = $reflection->getName();
|
||||
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
||||
|
||||
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
||||
}
|
||||
|
||||
$inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection);
|
||||
|
||||
$tool = new Tool($name, $inputSchema, $description, $data['annotations']);
|
||||
$registry->registerTool($tool, $data['handler'], true);
|
||||
|
||||
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
|
||||
$logger->debug("Registered manual tool {$name} from handler {$handlerDesc}");
|
||||
} catch (\Throwable $e) {
|
||||
$logger->error('Failed to register manual tool', ['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e]);
|
||||
throw new ConfigurationException("Error registering manual tool '{$data['name']}': {$e->getMessage()}", 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
// Register Resources
|
||||
foreach ($this->manualResources as $data) {
|
||||
try {
|
||||
$reflection = HandlerResolver::resolve($data['handler']);
|
||||
|
||||
if ($reflection instanceof \ReflectionFunction) {
|
||||
$name = $data['name'] ?? 'closure_resource_'.spl_object_id($data['handler']);
|
||||
$description = $data['description'] ?? null;
|
||||
} else {
|
||||
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
||||
$methodName = $reflection->getName();
|
||||
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
||||
|
||||
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
||||
}
|
||||
|
||||
$uri = $data['uri'];
|
||||
$mimeType = $data['mimeType'];
|
||||
$size = $data['size'];
|
||||
$annotations = $data['annotations'];
|
||||
|
||||
$resource = new Resource($uri, $name, $description, $mimeType, $annotations, $size);
|
||||
$registry->registerResource($resource, $data['handler'], true);
|
||||
|
||||
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
|
||||
$logger->debug("Registered manual resource {$name} from handler {$handlerDesc}");
|
||||
} catch (\Throwable $e) {
|
||||
$logger->error('Failed to register manual resource', ['handler' => $data['handler'], 'uri' => $data['uri'], 'exception' => $e]);
|
||||
throw new ConfigurationException("Error registering manual resource '{$data['uri']}': {$e->getMessage()}", 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
// Register Templates
|
||||
foreach ($this->manualResourceTemplates as $data) {
|
||||
try {
|
||||
$reflection = HandlerResolver::resolve($data['handler']);
|
||||
|
||||
if ($reflection instanceof \ReflectionFunction) {
|
||||
$name = $data['name'] ?? 'closure_template_'.spl_object_id($data['handler']);
|
||||
$description = $data['description'] ?? null;
|
||||
} else {
|
||||
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
||||
$methodName = $reflection->getName();
|
||||
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
||||
|
||||
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
||||
}
|
||||
|
||||
$uriTemplate = $data['uriTemplate'];
|
||||
$mimeType = $data['mimeType'];
|
||||
$annotations = $data['annotations'];
|
||||
|
||||
$template = new ResourceTemplate($uriTemplate, $name, $description, $mimeType, $annotations);
|
||||
$completionProviders = $this->getCompletionProviders($reflection);
|
||||
$registry->registerResourceTemplate($template, $data['handler'], $completionProviders, true);
|
||||
|
||||
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
|
||||
$logger->debug("Registered manual template {$name} from handler {$handlerDesc}");
|
||||
} catch (\Throwable $e) {
|
||||
$logger->error('Failed to register manual template', ['handler' => $data['handler'], 'uriTemplate' => $data['uriTemplate'], 'exception' => $e]);
|
||||
throw new ConfigurationException("Error registering manual resource template '{$data['uriTemplate']}': {$e->getMessage()}", 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
// Register Prompts
|
||||
foreach ($this->manualPrompts as $data) {
|
||||
try {
|
||||
$reflection = HandlerResolver::resolve($data['handler']);
|
||||
|
||||
if ($reflection instanceof \ReflectionFunction) {
|
||||
$name = $data['name'] ?? 'closure_prompt_'.spl_object_id($data['handler']);
|
||||
$description = $data['description'] ?? null;
|
||||
} else {
|
||||
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
||||
$methodName = $reflection->getName();
|
||||
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
||||
|
||||
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
||||
}
|
||||
|
||||
$arguments = [];
|
||||
$paramTags = $reflection instanceof \ReflectionMethod ? $docBlockParser->getParamTags($docBlockParser->parseDocBlock($reflection->getDocComment() ?? null)) : [];
|
||||
foreach ($reflection->getParameters() as $param) {
|
||||
$reflectionType = $param->getType();
|
||||
|
||||
// Basic DI check (heuristic)
|
||||
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$paramTag = $paramTags['$'.$param->getName()] ?? null;
|
||||
$arguments[] = new PromptArgument(
|
||||
$param->getName(),
|
||||
$paramTag ? trim((string) $paramTag->getDescription()) : null,
|
||||
!$param->isOptional() && !$param->isDefaultValueAvailable()
|
||||
);
|
||||
}
|
||||
|
||||
$prompt = new Prompt($name, $description, $arguments);
|
||||
$completionProviders = $this->getCompletionProviders($reflection);
|
||||
$registry->registerPrompt($prompt, $data['handler'], $completionProviders, true);
|
||||
|
||||
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
|
||||
$logger->debug("Registered manual prompt {$name} from handler {$handlerDesc}");
|
||||
} catch (\Throwable $e) {
|
||||
$logger->error('Failed to register manual prompt', ['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e]);
|
||||
throw new ConfigurationException("Error registering manual prompt '{$data['name']}': {$e->getMessage()}", 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
$logger->debug('Manual element registration complete.');
|
||||
}
|
||||
|
||||
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
|
||||
{
|
||||
$completionProviders = [];
|
||||
foreach ($reflection->getParameters() as $param) {
|
||||
$reflectionType = $param->getType();
|
||||
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$completionAttributes = $param->getAttributes(CompletionProvider::class, \ReflectionAttribute::IS_INSTANCEOF);
|
||||
if (!empty($completionAttributes)) {
|
||||
$attributeInstance = $completionAttributes[0]->newInstance();
|
||||
|
||||
if ($attributeInstance->provider) {
|
||||
$completionProviders[$param->getName()] = $attributeInstance->provider;
|
||||
} elseif ($attributeInstance->providerClass) {
|
||||
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
|
||||
} elseif ($attributeInstance->values) {
|
||||
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
|
||||
} elseif ($attributeInstance->enum) {
|
||||
$completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $completionProviders;
|
||||
}
|
||||
}
|
||||
75
src/Server/Transport/StdioTransport.php
Normal file
75
src/Server/Transport/StdioTransport.php
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?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\Server\Transport;
|
||||
|
||||
use Mcp\Server\TransportInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* Heavily inspired by https://jolicode.com/blog/mcp-the-open-protocol-that-turns-llm-chatbots-into-intelligent-agents.
|
||||
*/
|
||||
class StdioTransport implements TransportInterface
|
||||
{
|
||||
private string $buffer = '';
|
||||
|
||||
/**
|
||||
* @param resource $input
|
||||
* @param resource $output
|
||||
*/
|
||||
public function __construct(
|
||||
private $input = \STDIN,
|
||||
private $output = \STDOUT,
|
||||
private readonly LoggerInterface $logger = new NullLogger(),
|
||||
) {
|
||||
}
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function receive(): \Generator
|
||||
{
|
||||
$line = fgets($this->input);
|
||||
|
||||
$this->logger->debug('Received message on StdioTransport.', [
|
||||
'line' => $line,
|
||||
]);
|
||||
|
||||
if (false === $line) {
|
||||
return;
|
||||
}
|
||||
$this->buffer .= rtrim($line).\PHP_EOL;
|
||||
if (str_contains($this->buffer, \PHP_EOL)) {
|
||||
$lines = explode(\PHP_EOL, $this->buffer);
|
||||
$this->buffer = array_pop($lines);
|
||||
|
||||
yield from $lines;
|
||||
}
|
||||
}
|
||||
|
||||
public function send(string $data): void
|
||||
{
|
||||
$this->logger->debug('Sending data to client via StdioTransport.', ['data' => $data]);
|
||||
|
||||
fwrite($this->output, $data.\PHP_EOL);
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue