Skip to content

Commit 7bf76c2

Browse files
committed
feat(server): add support for annotation tools in HyperfMcpServer
1 parent 34bd170 commit 7bf76c2

6 files changed

Lines changed: 615 additions & 4 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
8+
namespace Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\Annotations;
9+
10+
use Attribute;
11+
use Dtyq\PhpMcp\Shared\Exceptions\ToolError;
12+
use Dtyq\PhpMcp\Shared\Utilities\ToolUtils;
13+
use Hyperf\Di\Annotation\AbstractAnnotation;
14+
15+
/**
16+
* @Annotation
17+
* @Target({"METHOD"})
18+
*/
19+
#[Attribute(Attribute::TARGET_METHOD)]
20+
class McpTool extends AbstractAnnotation
21+
{
22+
protected string $name = '';
23+
24+
protected string $description = '';
25+
26+
/** @var array<string, mixed> */
27+
protected array $inputSchema = [];
28+
29+
protected string $group = '';
30+
31+
protected bool $enabled = true;
32+
33+
/**
34+
* @param array<string, mixed> $inputSchema
35+
*/
36+
public function __construct(
37+
string $name = '',
38+
string $description = '',
39+
array $inputSchema = [],
40+
string $group = '',
41+
bool $enabled = true,
42+
) {
43+
if ($name !== '' && ! preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
44+
throw new ToolError('Tool name must be alphanumeric and underscores.');
45+
}
46+
$this->name = $name;
47+
$this->description = $description;
48+
$this->inputSchema = $inputSchema;
49+
$this->group = $group;
50+
$this->enabled = $enabled;
51+
}
52+
53+
public function collectMethod(string $className, ?string $target): void
54+
{
55+
if ($this->name === '') {
56+
$this->name = $target;
57+
}
58+
if (empty($this->inputSchema)) {
59+
$this->inputSchema = ToolUtils::generateInputSchema($className, $target);
60+
}
61+
parent::collectMethod($className, $target);
62+
}
63+
64+
public function getName(): string
65+
{
66+
return $this->name;
67+
}
68+
69+
public function getDescription(): string
70+
{
71+
return $this->description;
72+
}
73+
74+
/**
75+
* @return array<string, mixed>
76+
*/
77+
public function getInputSchema(): array
78+
{
79+
return $this->inputSchema;
80+
}
81+
82+
public function getGroup(): string
83+
{
84+
return $this->group;
85+
}
86+
87+
public function isEnabled(): bool
88+
{
89+
return $this->enabled;
90+
}
91+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
8+
namespace Dtyq\PhpMcp\Server\Framework\Hyperf\Collector;
9+
10+
use Dtyq\PhpMcp\Server\FastMcp\Tools\RegisteredTool;
11+
use Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\Annotations\McpTool;
12+
use Dtyq\PhpMcp\Types\Tools\Tool;
13+
use Hyperf\Context\ApplicationContext;
14+
use Hyperf\Di\Annotation\AnnotationCollector;
15+
use RuntimeException;
16+
17+
class McpCollector
18+
{
19+
protected static bool $collect = false;
20+
21+
/**
22+
* @var array<string, array<string, RegisteredTool>>
23+
*/
24+
protected static array $tools = [];
25+
26+
/**
27+
* @return array<string, RegisteredTool>
28+
*/
29+
public static function getTools(string $group = ''): array
30+
{
31+
self::collect();
32+
return self::$tools[$group] ?? [];
33+
}
34+
35+
public static function collect(): void
36+
{
37+
if (self::$collect) {
38+
return;
39+
}
40+
41+
self::collectTools();
42+
43+
self::$collect = true;
44+
}
45+
46+
protected static function collectTools(): void
47+
{
48+
$mcpToolAnnotations = AnnotationCollector::getMethodsByAnnotation(McpTool::class);
49+
var_dump($mcpToolAnnotations);
50+
foreach ($mcpToolAnnotations as $data) {
51+
$class = $data['class'] ?? '';
52+
$method = $data['method'] ?? '';
53+
/** @var McpTool $mcpTool */
54+
$mcpTool = $data['annotation'] ?? null;
55+
if (empty($class) || empty($method) || empty($mcpTool)) {
56+
continue;
57+
}
58+
if (! $mcpTool->isEnabled()) {
59+
continue;
60+
}
61+
$registeredTool = new RegisteredTool(
62+
new Tool(
63+
$mcpTool->getName(),
64+
$mcpTool->getInputSchema(),
65+
$mcpTool->getDescription()
66+
),
67+
function (array $arguments) use ($class, $method) {
68+
$container = ApplicationContext::getContainer();
69+
if (method_exists($container, 'make')) {
70+
$instance = $container->make($class);
71+
}
72+
if (! isset($instance) || ! method_exists($instance, $method)) {
73+
throw new RuntimeException("Method {$method} does not exist in class {$class}");
74+
}
75+
return $instance->{$method}(...$arguments);
76+
}
77+
);
78+
self::$tools[$mcpTool->getGroup()][$mcpTool->getName()] = $registeredTool;
79+
}
80+
}
81+
}

src/Server/Framework/Hyperf/HyperfMcpServer.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace Dtyq\PhpMcp\Server\Framework\Hyperf;
99

10+
use Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\McpCollector;
1011
use Dtyq\PhpMcp\Server\McpServer;
1112
use Dtyq\PhpMcp\Server\Transports\Http\SessionManagerInterface;
1213
use Dtyq\PhpMcp\Shared\Auth\AuthenticatorInterface;
@@ -17,7 +18,7 @@
1718

1819
class HyperfMcpServer
1920
{
20-
public function handler(): ResponseInterface
21+
public function handler(string $group = ''): ResponseInterface
2122
{
2223
$container = ApplicationContext::getContainer();
2324
$request = $container->get(RequestInterface::class);
@@ -27,6 +28,16 @@ public function handler(): ResponseInterface
2728
$app = new Application($container);
2829
$mcpServer = new McpServer('HyperfMcpServer', '1.0.0', $app);
2930

31+
$this->addAnnotationTools($mcpServer, $group);
32+
3033
return $mcpServer->http($request, $sessionManager, $authenticator);
3134
}
35+
36+
protected function addAnnotationTools(McpServer $mcpServer, string $group = ''): void
37+
{
38+
$registeredTools = McpCollector::getTools($group);
39+
foreach ($registeredTools as $registeredTool) {
40+
$mcpServer->registerTool($registeredTool);
41+
}
42+
}
3243
}

src/Server/Framework/Hyperf/RedisSessionManager.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Dtyq\PhpMcp\Shared\Kernel\Packer\PackerInterface;
1212
use Hyperf\Redis\RedisFactory;
1313
use Hyperf\Redis\RedisProxy;
14-
use Psr\Container\ContainerInterface;
1514

1615
/**
1716
* Redis-based session manager implementation.
@@ -39,11 +38,11 @@ class RedisSessionManager implements SessionManagerInterface
3938
private PackerInterface $packer;
4039

4140
public function __construct(
42-
ContainerInterface $container,
41+
PackerInterface $packer,
4342
RedisFactory $redisFactory,
4443
?int $sessionTtl = null
4544
) {
46-
$this->packer = $container->get(PackerInterface::class);
45+
$this->packer = $packer;
4746
$this->redisProxy = $redisFactory->get('default');
4847
if ($sessionTtl !== null) {
4948
$this->sessionTtl = $sessionTtl;

0 commit comments

Comments
 (0)