Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.00% covered (success)
90.00%
36 / 40
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
McpSdkRuntime
90.00% covered (success)
90.00%
36 / 40
60.00% covered (warning)
60.00%
3 / 5
10.10
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 runConsole
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getServerInfo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 faqSearch
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 buildServer
86.96% covered (success)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
3.02
1<?php
2
3/**
4 * phpMyFAQ MCP SDK Runtime
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-03-18
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Service\McpServer;
21
22use Mcp\Server;
23use Mcp\Server\Transport\StdioTransport;
24use RuntimeException;
25use Symfony\Component\Console\Input\InputInterface;
26use Symfony\Component\Console\Output\OutputInterface;
27
28final readonly class McpSdkRuntime implements McpServerRuntimeInterface
29{
30    /**
31     * @param array<string, mixed> $serverInfo
32     */
33    public function __construct(
34        private FaqSearchTool $faqSearchTool,
35        private array $serverInfo,
36    ) {
37    }
38
39    public function runConsole(InputInterface $input, OutputInterface $output): void
40    {
41        $this->buildServer()->run(new StdioTransport());
42    }
43
44    public function getServerInfo(): array
45    {
46        return $this->serverInfo;
47    }
48
49    /**
50     * Adapter method for mcp/sdk manual tool registration.
51     *
52     * Returns the decoded JSON payload for successful searches and a string for
53     * user-facing errors, matching the intent of the current Symfony MCP runtime.
54     *
55     * @return array<string, mixed>|string
56     */
57    public function faqSearch(
58        string $query,
59        ?int $category_id = null,
60        int $limit = 10,
61        bool $all_languages = false,
62    ): array|string {
63        $result = $this->faqSearchTool->execute([
64            'query' => $query,
65            'category_id' => $category_id,
66            'limit' => $limit,
67            'all_languages' => $all_languages,
68        ]);
69
70        $content = (string) ($result['content'] ?? '');
71        $decoded = json_decode($content, associative: true);
72        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
73            $normalized = [];
74            foreach ($decoded as $decodedKey => $decodedValue) {
75                $normalized[(string) $decodedKey] = $decodedValue;
76            }
77
78            return $normalized;
79        }
80
81        return $content;
82    }
83
84    private function buildServer(): Server
85    {
86        if (!class_exists(Server::class) || !class_exists(StdioTransport::class)) {
87            throw new RuntimeException(
88                'The mcp/sdk package is not installed or does not expose the expected server classes.',
89            );
90        }
91
92        $definition = $this->faqSearchTool->getDefinition();
93
94        return Server::builder()
95            ->setServerInfo(
96                (string) $this->serverInfo['name'],
97                (string) $this->serverInfo['version'],
98                (string) ($this->serverInfo['description'] ?? null),
99            )
100            ->addTool(
101                $this->faqSearch(...),
102                $definition->name,
103                null,
104                $definition->description,
105                null,
106                $definition->inputSchema,
107                null,
108                null,
109                $definition->outputSchema,
110            )
111            ->build();
112    }
113}