Skip to content

Commit bfaf605

Browse files
committed
refactor: extract MCP handlers from Server class into separate handler classes
Benefits: - Better separation of concerns with domain-specific handlers - Enhanced testability through interface-based dependency injection - Cleaner Server class that acts as coordinator rather than handling all logic - Consistent error handling and logging patterns across handler types - Easier maintenance and extension of MCP functionality
1 parent de482a7 commit bfaf605

File tree

11 files changed

+346
-156
lines changed

11 files changed

+346
-156
lines changed

src/McpServer/McpServerBootloader.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@
3838
use Butschster\ContextGenerator\McpServer\ProjectService\ProjectServiceInterface;
3939
use Butschster\ContextGenerator\McpServer\Prompt\McpPromptBootloader;
4040
use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry;
41+
use Butschster\ContextGenerator\McpServer\Routing\Handler\Prompts\PromptsHandler;
42+
use Butschster\ContextGenerator\McpServer\Routing\Handler\Prompts\PromptsHandlerInterface;
43+
use Butschster\ContextGenerator\McpServer\Routing\Handler\Resources\ResourcesHandler;
44+
use Butschster\ContextGenerator\McpServer\Routing\Handler\Resources\ResourcesHandlerInterface;
45+
use Butschster\ContextGenerator\McpServer\Routing\Handler\Tools\ToolsHandler;
46+
use Butschster\ContextGenerator\McpServer\Routing\Handler\Tools\ToolsHandlerInterface;
4147
use Butschster\ContextGenerator\McpServer\Routing\McpResponseStrategy;
4248
use Butschster\ContextGenerator\McpServer\Routing\RouteRegistrar;
4349
use Butschster\ContextGenerator\McpServer\Tool\McpToolBootloader;
@@ -116,6 +122,12 @@ public function boot(ConsoleBootloader $console): void
116122
public function defineSingletons(): array
117123
{
118124
return [
125+
// MCP Handlers
126+
PromptsHandlerInterface::class => PromptsHandler::class,
127+
ResourcesHandlerInterface::class => ResourcesHandler::class,
128+
ToolsHandlerInterface::class => ToolsHandler::class,
129+
130+
// Server infrastructure
119131
ServerRunnerInterface::class => function (
120132
McpConfig $config,
121133
ServerRunner $factory,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Routing\Handler;
6+
7+
use Butschster\ContextGenerator\McpServer\ProjectService\ProjectServiceInterface;
8+
use Butschster\ContextGenerator\McpServer\Routing\Mcp2PsrRequestAdapter;
9+
use Laminas\Diactoros\Response\JsonResponse;
10+
use League\Route\Router;
11+
use Psr\Log\LoggerInterface;
12+
13+
/**
14+
* Abstract base class providing common functionality for all MCP handlers
15+
*/
16+
abstract readonly class BaseHandler
17+
{
18+
public function __construct(
19+
protected Router $router,
20+
protected LoggerInterface $logger,
21+
protected ProjectServiceInterface $projectService,
22+
protected Mcp2PsrRequestAdapter $requestAdapter,
23+
) {}
24+
25+
/**
26+
* Handle a route using the router system
27+
*/
28+
protected function handleRoute(string $method, array $params = []): mixed
29+
{
30+
$this->logger->debug("Handling route: $method", $params);
31+
32+
// Create PSR request from MCP method and params
33+
$request = $this->requestAdapter->createPsrRequest($method, $params);
34+
35+
try {
36+
$response = $this->router->dispatch($request);
37+
\assert($response instanceof JsonResponse);
38+
39+
// Convert the response back to appropriate MCP type
40+
return $this->projectService->processResponse($response->getPayload());
41+
} catch (\Throwable $e) {
42+
$this->logger->error('Route handling error', [
43+
'method' => $method,
44+
'error' => $e->getMessage(),
45+
'trace' => $e->getTraceAsString(),
46+
]);
47+
48+
throw $e;
49+
}
50+
}
51+
52+
/**
53+
* Log error and return error result
54+
*/
55+
protected function logError(string $context, string $method, \Throwable $e): void
56+
{
57+
$this->logger->error("$context error", [
58+
'method' => $method,
59+
'error' => $e->getMessage(),
60+
'trace' => $e->getTraceAsString(),
61+
]);
62+
}
63+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Routing\Handler\Prompts;
6+
7+
use Butschster\ContextGenerator\McpServer\Routing\Handler\BaseHandler;
8+
use Laminas\Diactoros\Response\JsonResponse;
9+
use Mcp\Types\GetPromptRequestParams;
10+
use Mcp\Types\GetPromptResult;
11+
use Mcp\Types\ListPromptsResult;
12+
13+
/**
14+
* Handler for prompt-related MCP requests
15+
*/
16+
final readonly class PromptsHandler extends BaseHandler implements PromptsHandlerInterface
17+
{
18+
#[\Override]
19+
public function handlePromptsList(): ListPromptsResult
20+
{
21+
try {
22+
return $this->handleRoute('prompts/list');
23+
} catch (\Throwable $e) {
24+
$this->logError('Prompts list', 'prompts/list', $e);
25+
return new ListPromptsResult([]);
26+
}
27+
}
28+
29+
#[\Override]
30+
public function handlePromptsGet(GetPromptRequestParams $params): GetPromptResult
31+
{
32+
$params = $this->projectService->processPromptRequestParams($params);
33+
34+
$name = $params->name;
35+
$arguments = $params->arguments;
36+
$method = 'prompt/' . $name;
37+
38+
$this->logger->debug('Handling prompt get', [
39+
'prompt' => $name,
40+
'method' => $method,
41+
'arguments' => (array) $arguments->jsonSerialize(),
42+
]);
43+
44+
try {
45+
// Create PSR request with the prompt name in the path and arguments as POST body
46+
$request = $this->requestAdapter->createPsrRequest($method, (array) $arguments->jsonSerialize());
47+
48+
$response = $this->router->dispatch($request);
49+
\assert($response instanceof JsonResponse);
50+
51+
return $response->getPayload();
52+
} catch (\Throwable $e) {
53+
$this->logError('Prompt get', $method, $e);
54+
return new GetPromptResult([]);
55+
}
56+
}
57+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Routing\Handler\Prompts;
6+
7+
use Mcp\Types\GetPromptRequestParams;
8+
use Mcp\Types\GetPromptResult;
9+
use Mcp\Types\ListPromptsResult;
10+
11+
/**
12+
* Interface for handling prompt-related MCP requests
13+
*/
14+
interface PromptsHandlerInterface
15+
{
16+
/**
17+
* Handle prompts/list request
18+
*/
19+
public function handlePromptsList(): ListPromptsResult;
20+
21+
/**
22+
* Handle prompts/get request
23+
*/
24+
public function handlePromptsGet(GetPromptRequestParams $params): GetPromptResult;
25+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Routing\Handler\Resources;
6+
7+
use Butschster\ContextGenerator\McpServer\Routing\Handler\BaseHandler;
8+
use Laminas\Diactoros\Response\JsonResponse;
9+
use Mcp\Types\ListResourcesResult;
10+
use Mcp\Types\ReadResourceRequestParams;
11+
use Mcp\Types\ReadResourceResult;
12+
13+
/**
14+
* Handler for resource-related MCP requests
15+
*/
16+
final readonly class ResourcesHandler extends BaseHandler implements ResourcesHandlerInterface
17+
{
18+
#[\Override]
19+
public function handleResourcesList(): ListResourcesResult
20+
{
21+
try {
22+
return $this->handleRoute('resources/list');
23+
} catch (\Throwable $e) {
24+
$this->logError('Resources list', 'resources/list', $e);
25+
return new ListResourcesResult([]);
26+
}
27+
}
28+
29+
#[\Override]
30+
public function handleResourcesRead(ReadResourceRequestParams $params): ReadResourceResult
31+
{
32+
$params = $this->projectService->processResourceRequestParams($params);
33+
34+
[$type, $path] = \explode('://', $params->uri, 2);
35+
$method = 'resource/' . $type . '/' . $path;
36+
37+
$this->logger->debug('Handling resource read', [
38+
'resource' => $params->uri,
39+
'type' => $type,
40+
'path' => $path,
41+
'method' => $method,
42+
]);
43+
44+
try {
45+
// Create PSR request with the resource path
46+
$request = $this->requestAdapter->createPsrRequest($method);
47+
48+
$response = $this->router->dispatch($request);
49+
\assert($response instanceof JsonResponse);
50+
51+
return $response->getPayload();
52+
} catch (\Throwable $e) {
53+
$this->logError('Resource read', $method, $e);
54+
return new ReadResourceResult([]);
55+
}
56+
}
57+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Routing\Handler\Resources;
6+
7+
use Mcp\Types\ListResourcesResult;
8+
use Mcp\Types\ReadResourceRequestParams;
9+
use Mcp\Types\ReadResourceResult;
10+
11+
/**
12+
* Interface for handling resource-related MCP requests
13+
*/
14+
interface ResourcesHandlerInterface
15+
{
16+
/**
17+
* Handle resources/list request
18+
*/
19+
public function handleResourcesList(): ListResourcesResult;
20+
21+
/**
22+
* Handle resources/read request
23+
*/
24+
public function handleResourcesRead(ReadResourceRequestParams $params): ReadResourceResult;
25+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Routing\Handler\Tools;
6+
7+
use Butschster\ContextGenerator\McpServer\Routing\Handler\BaseHandler;
8+
use Laminas\Diactoros\Response\JsonResponse;
9+
use Mcp\Types\CallToolRequestParams;
10+
use Mcp\Types\CallToolResult;
11+
use Mcp\Types\ListToolsResult;
12+
use Mcp\Types\TextContent;
13+
14+
/**
15+
* Handler for tool-related MCP requests
16+
*/
17+
final readonly class ToolsHandler extends BaseHandler implements ToolsHandlerInterface
18+
{
19+
#[\Override]
20+
public function handleToolsList(): ListToolsResult
21+
{
22+
try {
23+
return $this->handleRoute('tools/list');
24+
} catch (\Throwable $e) {
25+
$this->logError('Tools list', 'tools/list', $e);
26+
return new ListToolsResult([]);
27+
}
28+
}
29+
30+
#[\Override]
31+
public function handleToolsCall(CallToolRequestParams $params): CallToolResult
32+
{
33+
$params = $this->projectService->processToolRequestParams($params);
34+
35+
$method = 'tools/call/' . $params->name;
36+
$arguments = $params->arguments ?? [];
37+
38+
$this->logger->debug('Handling tool call', [
39+
'tool' => $params->name,
40+
'method' => $method,
41+
'arguments' => $arguments,
42+
]);
43+
44+
try {
45+
// Create PSR request with the tool name in the path and arguments as POST body
46+
$request = $this->requestAdapter->createPsrRequest($method, $arguments);
47+
48+
$response = $this->router->dispatch($request);
49+
\assert($response instanceof JsonResponse);
50+
51+
return $response->getPayload();
52+
} catch (\Throwable $e) {
53+
$this->logError('Tool call', $method, $e);
54+
return new CallToolResult([new TextContent(text: $e->getMessage())], isError: true);
55+
}
56+
}
57+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Routing\Handler\Tools;
6+
7+
use Mcp\Types\CallToolRequestParams;
8+
use Mcp\Types\CallToolResult;
9+
use Mcp\Types\ListToolsResult;
10+
11+
/**
12+
* Interface for handling tool-related MCP requests
13+
*/
14+
interface ToolsHandlerInterface
15+
{
16+
/**
17+
* Handle tools/list request
18+
*/
19+
public function handleToolsList(): ListToolsResult;
20+
21+
/**
22+
* Handle tools/call request
23+
*/
24+
public function handleToolsCall(CallToolRequestParams $params): CallToolResult;
25+
}

0 commit comments

Comments
 (0)