This guide will help you quickly integrate PHP MCP Server into the Hyperf framework.
composer require dtyq/php-mcpAdd MCP route in your route file (e.g., config/routes.php):
<?php
use Hyperf\HttpServer\Router\Router;
use Dtyq\PhpMcp\Server\Framework\Hyperf\HyperfMcpServer;
Router::addRoute(['POST', 'GET', 'DELETE'], '/mcp', function () {
return \Hyperf\Context\ApplicationContext::getContainer()->get(HyperfMcpServer::class)->handle('default');
});Note: ConfigProvider is auto-loaded by Hyperf, no need to manually register it in
config/config.php.
The easiest way to register MCP tools, prompts, and resources is using annotations. This approach automatically generates schemas from method signatures and handles registration.
Use the #[McpTool] annotation to register methods as MCP tools:
<?php
declare(strict_types=1);
namespace App\Service;
use Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\Annotations\McpTool;
class CalculatorService
{
#[McpTool]
public function calculate(string $operation, int $a, int $b): array
{
$result = match ($operation) {
'add' => $a + $b,
'subtract' => $a - $b,
'multiply' => $a * $b,
'divide' => $a / $b,
default => null,
};
return [
'operation' => $operation,
'operands' => [$a, $b],
'result' => $result,
];
}
#[McpTool(
name: 'advanced_calc',
description: 'Advanced mathematical calculations',
server: 'math'
)]
public function advancedCalculate(string $formula, array $variables = []): float
{
// Complex calculation logic
return 42.0;
}
}Annotation Parameters:
name: Tool name (defaults to method name)description: Tool descriptioninputSchema: Custom input schema (auto-generated if empty)server: MCP server name used for registrationversion: MCP server version used for registrationenabled: Whether the tool is enabled (default: true)
Use the #[McpPrompt] annotation to register methods as prompt templates:
<?php
declare(strict_types=1);
namespace App\Service;
use Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\Annotations\McpPrompt;
use Dtyq\PhpMcp\Types\Prompts\GetPromptResult;
use Dtyq\PhpMcp\Types\Prompts\PromptMessage;
use Dtyq\PhpMcp\Types\Content\TextContent;
use Dtyq\PhpMcp\Types\Core\ProtocolConstants;
class PromptService
{
#[McpPrompt]
public function greeting(string $name, string $language = 'english'): GetPromptResult
{
$greetings = [
'english' => "Hello, {$name}! Welcome to the Streamable HTTP MCP server!",
'spanish' => "¡Hola, {$name}! ¡Bienvenido al servidor MCP Streamable HTTP!",
'french' => "Bonjour, {$name}! Bienvenue sur le serveur MCP Streamable HTTP!",
'chinese' => "你好,{$name}!欢迎使用 Streamable HTTP MCP 服务器!",
];
$message = new PromptMessage(
ProtocolConstants::ROLE_USER,
new TextContent($greetings[$language] ?? $greetings['english'])
);
return new GetPromptResult('Greeting prompt', [$message]);
}
#[McpPrompt(
name: 'code_review',
description: 'Generate code review prompts',
server: 'development'
)]
public function codeReview(string $code, string $language = 'php'): GetPromptResult
{
$prompt = "Please review the following {$language} code:\n\n```{$language}\n{$code}\n```\n\nProvide feedback on:\n- Code quality\n- Best practices\n- Potential improvements";
$message = new PromptMessage(
ProtocolConstants::ROLE_USER,
new TextContent($prompt)
);
return new GetPromptResult('Code review prompt', [$message]);
}
}Annotation Parameters:
name: Prompt name (defaults to method name)description: Prompt descriptionarguments: Custom arguments schema (auto-generated if empty)server: MCP server name used for registrationversion: MCP server version used for registrationenabled: Whether the prompt is enabled (default: true)
Use the #[McpResource] annotation to register methods as resource providers:
<?php
declare(strict_types=1);
namespace App\Service;
use Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\Annotations\McpResource;
use Dtyq\PhpMcp\Types\Resources\TextResourceContents;
class SystemService
{
#[McpResource]
public function systemInfo(): TextResourceContents
{
$info = [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'memory_usage' => memory_get_usage(true),
'timestamp' => date('c'),
'pid' => getmypid(),
];
return new TextResourceContents(
'mcp://system/info',
json_encode($info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
'application/json'
);
}
#[McpResource(
name: 'server_config',
uri: 'mcp://system/config',
description: 'Server configuration data',
mimeType: 'application/json'
)]
public function serverConfig(): TextResourceContents
{
$config = [
'environment' => env('APP_ENV', 'production'),
'debug' => env('APP_DEBUG', false),
'timezone' => date_default_timezone_get(),
];
return new TextResourceContents(
'mcp://system/config',
json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
'application/json'
);
}
}Annotation Parameters:
name: Resource name (defaults to method name)uri: Resource URI (auto-generated if empty)description: Resource descriptionmimeType: Resource MIME typesize: Resource size in bytesserver: MCP server name used for registrationversion: MCP server version used for registrationenabled: Whether the resource is enabled (default: true)isTemplate: Whether the resource is a templateuriTemplate: URI template parameters
The annotation system automatically generates JSON schemas from method signatures:
#[McpTool]
public function processUser(
string $userId, // Required string parameter
int $age = 18, // Optional integer with default value
bool $active = true, // Optional boolean with default value
array $tags = [] // Optional array with default empty array
): array {
// Implementation
}This generates the following schema:
{
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "Parameter: userId"
},
"age": {
"type": "integer",
"description": "Parameter: age",
"default": 18
},
"active": {
"type": "boolean",
"description": "Parameter: active",
"default": true
},
"tags": {
"type": "array",
"description": "Parameter: tags",
"items": {"type": "string"},
"default": []
}
},
"required": ["userId"]
}Supported Types:
string→"type": "string"int,integer→"type": "integer"float,double→"type": "number"bool,boolean→"type": "boolean"array→"type": "array"
Note: Complex types (classes, interfaces, union types) are not supported. Only basic PHP types are allowed for automatic schema generation.
You can organize your annotations by server name and load a specific MCP server on each route:
// Register the math MCP server
Router::addRoute(['POST', 'GET', 'DELETE'], '/mcp/math', function () {
return \Hyperf\Context\ApplicationContext::getContainer()->get(HyperfMcpServer::class)->handle('math');
});
// Register the development MCP server
Router::addRoute(['POST', 'GET', 'DELETE'], '/mcp/dev', function () {
return \Hyperf\Context\ApplicationContext::getContainer()->get(HyperfMcpServer::class)->handle('development');
});
// Register the default MCP server
Router::addRoute(['POST', 'GET', 'DELETE'], '/mcp', function () {
return \Hyperf\Context\ApplicationContext::getContainer()->get(HyperfMcpServer::class)->handle('default');
});Here's a complete service class using all three annotation types:
<?php
declare(strict_types=1);
namespace App\Service;
use Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\Annotations\McpTool;
use Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\Annotations\McpPrompt;
use Dtyq\PhpMcp\Server\Framework\Hyperf\Collector\Annotations\McpResource;
use Dtyq\PhpMcp\Types\Prompts\GetPromptResult;
use Dtyq\PhpMcp\Types\Prompts\PromptMessage;
use Dtyq\PhpMcp\Types\Content\TextContent;
use Dtyq\PhpMcp\Types\Core\ProtocolConstants;
use Dtyq\PhpMcp\Types\Resources\TextResourceContents;
class McpDemoService
{
#[McpTool(description: 'Echo back a message')]
public function echo(string $message): array
{
return [
'echo' => $message,
'timestamp' => time(),
];
}
#[McpPrompt(description: 'Generate a welcome message')]
public function welcome(string $username): GetPromptResult
{
$message = new PromptMessage(
ProtocolConstants::ROLE_USER,
new TextContent("Welcome {$username} to our MCP server!")
);
return new GetPromptResult('Welcome message', [$message]);
}
#[McpResource(description: 'Current server status')]
public function status(): TextResourceContents
{
$status = [
'status' => 'healthy',
'uptime' => time() - $_SERVER['REQUEST_TIME'],
'memory' => round(memory_get_usage() / 1024 / 1024, 2) . ' MB',
];
return new TextResourceContents(
'mcp://server/status',
json_encode($status, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
'application/json'
);
}
}If you need custom authentication, implement AuthenticatorInterface:
<?php
declare(strict_types=1);
namespace App\Auth;
use Dtyq\PhpMcp\Shared\Auth\AuthenticatorInterface;
use Dtyq\PhpMcp\Shared\Exceptions\AuthenticationError;
use Dtyq\PhpMcp\Types\Auth\AuthInfo;
use Hyperf\HttpServer\Contract\RequestInterface;
class CustomAuthenticator implements AuthenticatorInterface
{
public function __construct(
protected RequestInterface $request,
) {
}
public function authenticate(): AuthInfo
{
$apiKey = $this->request->header('X-API-Key');
// Implement your authentication logic
if (!$this->validateApiKey($apiKey)) {
throw new AuthenticationError('Authentication failed');
}
return AuthInfo::create(
subject: 'user-123',
scopes: ['read', 'write'],
metadata: ['api_key' => $apiKey]
);
}
private function validateApiKey(string $apiKey): bool
{
// Your API key validation logic
return $apiKey === 'your-secret-api-key';
}
}Then bind it in configuration:
// config/autoload/dependencies.php
return [
\Dtyq\PhpMcp\Shared\Auth\AuthenticatorInterface::class => App\Auth\CustomAuthenticator::class,
];You can listen to HttpTransportAuthenticatedEvent to dynamically register tools, resources and prompts:
<?php
declare(strict_types=1);
namespace App\Listener;
use App\Service\UserToolService;
use Dtyq\PhpMcp\Server\Transports\Http\Event\HttpTransportAuthenticatedEvent;
use Dtyq\PhpMcp\Types\Tools\Tool;
use Dtyq\PhpMcp\Types\Resources\Resource;
use Dtyq\PhpMcp\Types\Prompts\Prompt;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Psr\Container\ContainerInterface;
#[Listener]
class DynamicMcpResourcesListener implements ListenerInterface
{
public function __construct(
protected ContainerInterface $container,
) {
}
public function listen(): array
{
return [
HttpTransportAuthenticatedEvent::class,
];
}
public function process(object $event): void
{
if (!$event instanceof HttpTransportAuthenticatedEvent) {
return;
}
$transportMetadata = $event->getTransportMetadata();
$authInfo = $event->getAuthInfo();
// Get authenticated user information
$user = $authInfo->getMetadata('user');
$permissions = $authInfo->getMetadata('permissions', []);
// Dynamic tool registration
$this->registerDynamicTools($transportMetadata, $user, $permissions);
// Dynamic resource registration
$this->registerDynamicResources($transportMetadata, $user, $permissions);
// Dynamic prompt registration
$this->registerDynamicPrompts($transportMetadata, $user, $permissions);
}
private function registerDynamicTools($transportMetadata, $user, array $permissions): void
{
$toolManager = $transportMetadata->getToolManager();
// Register different tools based on user permissions
if (in_array('user_management', $permissions)) {
$userTool = new Tool('get_user_info', [
'type' => 'object',
'properties' => [
'user_id' => ['type' => 'integer'],
],
'required' => ['user_id'],
], 'Get user information');
$toolManager->register($userTool, function(array $args) use ($user) {
// Implement tool logic
return $this->container->get(UserToolService::class)->getUserInfo($args['user_id'], $user);
});
}
if (in_array('admin', $permissions)) {
$adminTool = new Tool('admin_operation', [
'type' => 'object',
'properties' => [
'action' => ['type' => 'string'],
'target' => ['type' => 'string'],
],
'required' => ['action'],
], 'Execute admin operations');
$toolManager->register($adminTool, function(array $args) {
// Admin-specific tool logic
return ['result' => "Admin action: {$args['action']}"];
});
}
}
private function registerDynamicResources($transportMetadata, $user, array $permissions): void
{
$resourceManager = $transportMetadata->getResourceManager();
// Register resources based on permissions
if (in_array('read_users', $permissions)) {
$usersResource = new Resource('users', 'application/json', 'Users list');
$resourceManager->register($usersResource, function() use ($user) {
// Return users list that user has permission to access
return json_encode(['users' => ['Alice', 'Bob']]);
});
}
if (in_array('read_reports', $permissions)) {
$reportsResource = new Resource('reports', 'application/json', 'Reports data');
$resourceManager->register($reportsResource, function() {
return json_encode(['reports' => ['report1', 'report2']]);
});
}
}
private function registerDynamicPrompts($transportMetadata, $user, array $permissions): void
{
$promptManager = $transportMetadata->getPromptManager();
// Register prompt templates based on user roles
if (in_array('content_creator', $permissions)) {
$contentPrompt = new Prompt('create_content', [
'type' => 'object',
'properties' => [
'topic' => ['type' => 'string'],
'style' => ['type' => 'string'],
],
'required' => ['topic'],
], 'Content creation prompt template');
$promptManager->register($contentPrompt, function(array $args) {
return [
'prompt' => "Please create content for topic '{$args['topic']}' with style: " . ($args['style'] ?? 'formal'),
];
});
}
}
}Tips:
- Dynamic registration via event listeners is more flexible than static registration, allowing different tools and resources based on user identity, permissions, etc.
- Annotation mechanism will be added in the future to simplify auto-registration process
- Tools, resources and prompts all support this dynamic registration approach
Redis is used for session management by default. Configure Redis connection in config/autoload/redis.php:
<?php
return [
'default' => [
'host' => env('REDIS_HOST', 'localhost'),
'auth' => env('REDIS_AUTH', null),
'port' => (int) env('REDIS_PORT', 6379),
'db' => (int) env('REDIS_DB', 0),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
],
],
];To customize session TTL, configure via dependency injection:
// config/autoload/dependencies.php
use Dtyq\PhpMcp\Server\Framework\Hyperf\RedisSessionManager;
use Dtyq\PhpMcp\Server\Transports\Http\SessionManagerInterface;
return [
SessionManagerInterface::class => function ($container) {
return new RedisSessionManager(
$container,
$container->get(\Hyperf\Redis\RedisFactory::class),
3600 // Set session TTL to 1 hour
);
},
];Here's a complete working Hyperf MCP server example:
hyperf-mcp-demo/
├── config/
│ ├── routes.php # Route configuration
│ └── autoload/
│ ├── dependencies.php # Dependency injection config
│ └── redis.php # Redis configuration
├── app/
│ ├── Auth/
│ │ └── ApiKeyAuthenticator.php # Custom authenticator
│ ├── Listener/
│ │ └── DynamicMcpListener.php # Dynamic registration listener
│ └── Service/
│ └── UserService.php # Business service
└── composer.json
<?php
use Hyperf\HttpServer\Router\Router;
use Dtyq\PhpMcp\Server\Framework\Hyperf\HyperfMcpServer;
// MCP server endpoint - just one line of code!
Router::addRoute(['POST', 'GET', 'DELETE'], '/mcp', function () {
return \Hyperf\Context\ApplicationContext::getContainer()->get(HyperfMcpServer::class)->handle('default');
});<?php
declare(strict_types=1);
namespace App\Auth;
use Dtyq\PhpMcp\Shared\Auth\AuthenticatorInterface;
use Dtyq\PhpMcp\Shared\Exceptions\AuthenticationError;
use Dtyq\PhpMcp\Types\Auth\AuthInfo;
use Hyperf\HttpServer\Contract\RequestInterface;
class ApiKeyAuthenticator implements AuthenticatorInterface
{
public function __construct(
protected RequestInterface $request,
) {
}
public function authenticate(): AuthInfo
{
$apiKey = $this->getRequestApiKey();
if (empty($apiKey)) {
throw new AuthenticationError('No API key provided');
}
// Validate API Key
$userInfo = $this->validateApiKey($apiKey);
if (!$userInfo) {
throw new AuthenticationError('Invalid API key');
}
return AuthInfo::create(
subject: $userInfo['user_id'],
scopes: $userInfo['scopes'],
metadata: [
'user' => $userInfo,
'permissions' => $userInfo['permissions'],
'api_key' => $apiKey,
]
);
}
private function getRequestApiKey(): string
{
// Support multiple ways to pass API Key
$apiKey = $this->request->header('authorization', $this->request->input('key', ''));
if (empty($apiKey)) {
// Also support X-API-Key header
$apiKey = $this->request->header('x-api-key', '');
}
if (empty($apiKey)) {
return '';
}
// Handle Bearer token format
if (str_starts_with($apiKey, 'Bearer ')) {
$apiKey = substr($apiKey, 7);
}
return $apiKey;
}
private function validateApiKey(string $apiKey): ?array
{
// Mock API Key validation logic
// In real projects, this should be database queries or external API calls
$validKeys = [
'admin-key-123' => [
'user_id' => 'admin',
'scopes' => ['*'],
'permissions' => ['admin', 'user_management', 'read_users', 'read_reports'],
],
'user-key-456' => [
'user_id' => 'user1',
'scopes' => ['read', 'write'],
'permissions' => ['read_users'],
],
];
return $validKeys[$apiKey] ?? null;
}
}<?php
declare(strict_types=1);
namespace App\Listener;
use App\Service\UserService;
use Dtyq\PhpMcp\Server\Transports\Http\Event\HttpTransportAuthenticatedEvent;
use Dtyq\PhpMcp\Types\Tools\Tool;
use Dtyq\PhpMcp\Types\Resources\Resource;
use Dtyq\PhpMcp\Types\Prompts\Prompt;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Psr\Container\ContainerInterface;
#[Listener]
class DynamicMcpListener implements ListenerInterface
{
public function __construct(
protected ContainerInterface $container,
) {
}
public function listen(): array
{
return [HttpTransportAuthenticatedEvent::class];
}
public function process(object $event): void
{
if (!$event instanceof HttpTransportAuthenticatedEvent) {
return;
}
$transportMetadata = $event->getTransportMetadata();
$authInfo = $event->getAuthInfo();
$permissions = $authInfo->getMetadata('permissions', []);
$userService = $this->container->get(UserService::class);
// Dynamic tool registration
$this->registerTools($transportMetadata, $authInfo, $permissions, $userService);
// Dynamic resource registration
$this->registerResources($transportMetadata, $authInfo, $permissions, $userService);
// Dynamic prompt registration
$this->registerPrompts($transportMetadata, $authInfo, $permissions);
}
private function registerTools($transportMetadata, $authInfo, array $permissions, UserService $userService): void
{
$toolManager = $transportMetadata->getToolManager();
// Basic tool - available to all users
$echoTool = new Tool('echo', [
'type' => 'object',
'properties' => ['message' => ['type' => 'string']],
'required' => ['message']
], 'Echo message');
$toolManager->register($echoTool, function(array $args) {
return ['response' => $args['message'], 'timestamp' => time()];
});
// User management tool - requires permission
if (in_array('user_management', $permissions)) {
$userTool = new Tool('get_user', [
'type' => 'object',
'properties' => ['user_id' => ['type' => 'string']],
'required' => ['user_id']
], 'Get user information');
$toolManager->register($userTool, function(array $args) use ($userService, $authInfo) {
return $userService->getUserInfo($args['user_id'], $authInfo);
});
}
// Admin tool
if (in_array('admin', $permissions)) {
$adminTool = new Tool('admin_stats', [
'type' => 'object',
'properties' => [],
'required' => []
], 'Get system statistics');
$toolManager->register($adminTool, function(array $args) use ($userService) {
return $userService->getSystemStats();
});
}
}
private function registerResources($transportMetadata, $authInfo, array $permissions, UserService $userService): void
{
$resourceManager = $transportMetadata->getResourceManager();
if (in_array('read_users', $permissions)) {
$usersResource = new Resource('users', 'application/json', 'Users list data');
$resourceManager->register($usersResource, function() use ($userService, $authInfo) {
return $userService->getUsersListJson($authInfo);
});
}
if (in_array('read_reports', $permissions)) {
$reportsResource = new Resource('reports', 'application/json', 'Reports data');
$resourceManager->register($reportsResource, function() use ($userService) {
return $userService->getReportsJson();
});
}
}
private function registerPrompts($transportMetadata, $authInfo, array $permissions): void
{
$promptManager = $transportMetadata->getPromptManager();
// Basic prompt template
$helpPrompt = new Prompt('help', [
'type' => 'object',
'properties' => [],
'required' => []
], 'Help information prompt');
$promptManager->register($helpPrompt, function(array $args) use ($authInfo) {
$userName = $authInfo->getSubject();
return [
'prompt' => "Hello {$userName}, I am the MCP assistant. I can help you with the following features:\n" .
"- echo: Echo messages\n" .
"- get_user: Get user information (requires permission)\n" .
"- admin_stats: System statistics (admin only)"
];
});
}
}<?php
declare(strict_types=1);
namespace App\Service;
use Dtyq\PhpMcp\Types\Auth\AuthInfo;
class UserService
{
public function getUserInfo(string $userId, AuthInfo $authInfo): array
{
// Mock user data
$users = [
'admin' => ['id' => 'admin', 'name' => 'Administrator', 'role' => 'admin'],
'user1' => ['id' => 'user1', 'name' => 'Alice', 'role' => 'user'],
'user2' => ['id' => 'user2', 'name' => 'Bob', 'role' => 'user'],
];
if (!isset($users[$userId])) {
throw ValidationError::requiredFieldMissing('user', 'User {$userId} not found');
}
return ['user' => $users[$userId]];
}
public function getUsersListJson(AuthInfo $authInfo): string
{
$permissions = $authInfo->getMetadata('permissions', []);
// Return different user lists based on permissions
if (in_array('admin', $permissions)) {
$users = [
['id' => 'admin', 'name' => 'Administrator', 'role' => 'admin'],
['id' => 'user1', 'name' => 'Alice', 'role' => 'user'],
['id' => 'user2', 'name' => 'Bob', 'role' => 'user'],
];
} else {
$users = [
['id' => 'user1', 'name' => 'Alice', 'role' => 'user'],
['id' => 'user2', 'name' => 'Bob', 'role' => 'user'],
];
}
return json_encode(['users' => $users]);
}
public function getReportsJson(): string
{
return json_encode([
'reports' => [
['id' => 1, 'title' => 'Daily Report', 'date' => date('Y-m-d')],
['id' => 2, 'title' => 'Weekly Report', 'date' => date('Y-m-d', strtotime('last monday'))],
]
]);
}
public function getSystemStats(): array
{
return [
'stats' => [
'total_users' => 3,
'active_sessions' => 1,
'uptime' => '2 hours',
'memory_usage' => round(memory_get_usage() / 1024 / 1024, 2) . ' MB',
]
];
}
}<?php
return [
\Dtyq\PhpMcp\Shared\Auth\AuthenticatorInterface::class => \App\Auth\ApiKeyAuthenticator::class,
];# 1. Initialize request (using admin API key)
curl -X POST http://localhost:9501/mcp \
-H "Content-Type: application/json" \
-H "X-API-Key: admin-key-123" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0.0"}
}
}'
# 2. List tools
curl -X POST http://localhost:9501/mcp \
-H "Content-Type: application/json" \
-H "X-API-Key: admin-key-123" \
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}'
# 3. Call tool
curl -X POST http://localhost:9501/mcp \
-H "Content-Type: application/json" \
-H "X-API-Key: admin-key-123" \
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {"message": "Hello Hyperf MCP!"}
}
}'
# 4. Read resource
curl -X POST http://localhost:9501/mcp \
-H "Content-Type: application/json" \
-H "X-API-Key: admin-key-123" \
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": {"uri": "users"}
}'This complete example demonstrates:
- ✅ API Key-based authentication
- ✅ Permission-based dynamic tool registration
- ✅ Session management
- ✅ Actually runnable code
- ✅ Complete testing workflow
Test your MCP server using cURL:
# Test tool call
curl -X POST http://localhost:9501/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {"message": "Hello, Hyperf MCP!"}
}
}'-
Redis Connection Failed
- Check if Redis service is running
- Verify Redis configuration is correct
-
Authentication Failed
- Ensure custom authenticator is implemented correctly
- Check if request headers contain required authentication information
-
Tool Not Found
- Ensure tools are registered correctly
- Check if tool names match
In development environment, you can enable detailed error logging:
// config/autoload/logger.php
return [
'default' => [
'handler' => [
'class' => \Monolog\Handler\StreamHandler::class,
'constructor' => [
'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
'level' => \Monolog\Logger::DEBUG,
],
],
'formatter' => [
'class' => \Monolog\Formatter\LineFormatter::class,
'constructor' => [
'format' => null,
'dateFormat' => 'Y-m-d H:i:s',
'allowInlineLineBreaks' => true,
],
],
],
];