Lines 99.00% 99 / 100
Methods 87.50% 7 / 8
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 getDefinition 100.00% 46 / 46 100.00% 1 / 1 1
 execute 96.87% 31 / 32 0.00% 0 / 1 7
 getSearch 100.00% 1 / 1 100.00% 1 / 1 1
 getFaq 100.00% 1 / 1 100.00% 1 / 1 1
 formatResultsAsJson 100.00% 13 / 13 100.00% 1 / 1 4
 buildFaqUrl 100.00% 1 / 1 100.00% 1 / 1 1
 createResult 100.00% 5 / 5 100.00% 1 / 1 1
28readonly class FaqSearchTool implements McpToolExecutorInterface
29{
30    public function __construct(
31        private Configuration $configuration,
32        private Search $search,
33        private Faq $faq,
34    ) {
35    }
36
37    public function getDefinition(): McpToolDefinition
38    {
39        return new McpToolDefinition(
40            name: 'faq_search',
41            description: 'Search through the phpMyFAQ knowledge base to find relevant FAQ entries that can answer questions. '
42            . 'This tool searches both questions and answers in the FAQ database to provide comprehensive results.',
43            title: 'FAQ Search',
44            inputSchema: [
45                'type' => 'object',
46                'properties' => [
47                    'query' => [
48                        'type' => 'string',
49                        'description' => 'The search query or question to find relevant FAQ entries for',
50                    ],
51                    'category_id' => [
52                        'type' => 'integer',
53                        'description' => 'Optional category ID to limit search to a specific FAQ category',
54                        'minimum' => 1,
55                    ],
56                    'limit' => [
57                        'type' => 'integer',
58                        'description' => 'Maximum number of results to return (default: 10)',
59                        'default' => 10,
60                        'minimum' => 1,
61                        'maximum' => 50,
62                    ],
63                    'all_languages' => [
64                        'type' => 'boolean',
65                        'description' => 'Whether to search in all languages or just the current language (default: false)',
66                        'default' => false,
67                    ],
68                ],
69                'required' => ['query'],
70            ],
71            outputSchema: [
72                'type' => 'object',
73                'properties' => [
74                    'results' => [
75                        'type' => 'array',
76                        'description' => 'Array of FAQ search results',
77                    ],
78                    'total_found' => [
79                        'type' => 'integer',
80                        'description' => 'Total number of FAQ entries found',
81                    ],
82                ],
83            ],
84        );
85    }
86
87    public function execute(array $arguments): array
88    {
89        $query = $arguments['query'] ?? '';
90        $categoryId = $arguments['category_id'] ?? null;
91        $limit = $arguments['limit'] ?? 10;
92        $allLanguages = $arguments['all_languages'] ?? false;
93
94        if (trim((string) $query) === '') {
95            return $this->createResult('Error: Search query cannot be empty.');
96        }
97
98        try {
99            $this->faq->setUser(-1);
100            $this->faq->setGroups([-1]);
101
102            $category = new Category($this->configuration, [-1]);
103            $category->setUser(-1);
104            $this->search->setCategory($category);
105
106            $this->search->setCategoryId($categoryId !== null ? (int) $categoryId : null);
107
108            $searchResults = $this->search->search((string) $query, (bool) $allLanguages);
109
110            if ($searchResults === []) {
111                return $this->createResult($this->formatResultsAsJson([]));
112            }
113
114            $validResults = [];
115            foreach ($searchResults as $searchResult) {
116                $validResults[] = [
117                    'id' => $searchResult->id,
118                    'language' => $searchResult->lang,
119                    'question' => $searchResult->question ?? '',
120                    'answer' => $searchResult->answer ?? '',
121                    'category_id' => $searchResult->category_id ?? null,
122                    'relevance_score' => $searchResult->score ?? 0.0,
123                    'url' => $this->buildFaqUrl((int) $searchResult->id, (string) $searchResult->lang),
124                ];
125            }
126
127            $limitedResults = array_slice($validResults, offset: 0, length: (int) $limit);
128
129            if ($limitedResults === []) {
130                return $this->createResult('No accessible FAQ entries found for the given query.');
131            }
132
133            return $this->createResult($this->formatResultsAsJson($limitedResults));
134        } catch (Exception $exception) {
135            return $this->createResult('Error searching FAQ database: ' . $exception->getMessage());
136        }
137    }
138
139    public function getSearch(): Search
140    {
141        return $this->search;
142    }
143
144    public function getFaq(): Faq
145    {
146        return $this->faq;
147    }
148
149    /**
150     * @param array<int, array<string, mixed>> $results
151     */
152    private function formatResultsAsJson(array $results): string
153    {
154        if ($results === []) {
155            $jsonData = [
156                'results' => [],
157                'total_found' => 0,
158            ];
159
160            $emptyJson = json_encode($jsonData);
161
162            return $emptyJson === false ? '{"results":[],"total_found":0}' : $emptyJson;
163        }
164
165        $jsonData = [
166            'results' => $results,
167            'total_found' => count($results),
168        ];
169
170        $json = json_encode($jsonData, JSON_PRETTY_PRINT);
171
172        return $json === false ? '{"results":[],"total_found":0}' : $json;
173    }
174
175    private function buildFaqUrl(int $faqId, string $language): string
176    {
177        return $this->configuration->getDefaultUrl() . 'content/' . $faqId . '/' . $language;
178    }
179
180    /**
181     * @return array{content: string, type: string, mimeType: string}
182     */
183    private function createResult(string $content): array
184    {
185        return [
186            'content' => $content,
187            'type' => 'text',
188            'mimeType' => 'application/json',
189        ];
190    }
191}