1111
1212namespace Piwik \Plugins \McpServer ;
1313
14+ use Matomo \Dependencies \McpServer \Mcp \Capability \Attribute \McpTool ;
15+ use Matomo \Dependencies \McpServer \Mcp \Capability \Discovery \DocBlockParser ;
16+ use Matomo \Dependencies \McpServer \Mcp \Capability \Discovery \SchemaGenerator ;
1417use Matomo \Dependencies \McpServer \Mcp \Capability \Registry ;
1518use Matomo \Dependencies \McpServer \Mcp \Capability \Registry \ReferenceHandler ;
1619use Matomo \Dependencies \McpServer \Mcp \Schema \ServerCapabilities ;
2831use Piwik \Plugins \McpServer \McpTools \ApiCallUpdate ;
2932use Piwik \Plugins \McpServer \McpTools \ApiGet ;
3033use Piwik \Plugins \McpServer \McpTools \ApiList ;
34+ use Piwik \Plugins \McpServer \McpTools \DimensionGet ;
35+ use Piwik \Plugins \McpServer \McpTools \DimensionList ;
36+ use Piwik \Plugins \McpServer \McpTools \GoalGet ;
37+ use Piwik \Plugins \McpServer \McpTools \GoalList ;
38+ use Piwik \Plugins \McpServer \McpTools \ReportList ;
39+ use Piwik \Plugins \McpServer \McpTools \ReportMetadata ;
3140use Piwik \Plugins \McpServer \McpTools \ReportProcessed ;
41+ use Piwik \Plugins \McpServer \McpTools \SegmentGet ;
42+ use Piwik \Plugins \McpServer \McpTools \SegmentList ;
43+ use Piwik \Plugins \McpServer \McpTools \SiteGet ;
44+ use Piwik \Plugins \McpServer \McpTools \SiteList ;
45+ use Piwik \Plugins \McpServer \McpTools \SiteSearch ;
3246use Piwik \Plugins \McpServer \Schemas \Api \ApiCallToolInputSchema ;
3347use Piwik \Plugins \McpServer \Schemas \Api \ApiCallToolOutputSchema ;
3448use Piwik \Plugins \McpServer \Schemas \Api \ApiGetToolInputSchema ;
@@ -70,7 +84,6 @@ public function createServer(): Server
7084 ->setRegistry ($ registry )
7185 ->setSession ($ this ->sessionStore )
7286 ->setContainer ($ this ->container )
73- ->setDiscovery (__DIR__ , ['McpTools ' ])
7487 ->setCapabilities (new ServerCapabilities (
7588 tools: true ,
7689 // Use null to avoid advertising listChanged capabilities we don't implement.
@@ -84,6 +97,8 @@ public function createServer(): Server
8497 completions: false ,
8598 ));
8699
100+ $ this ->registerAttributeTools ($ builder );
101+
87102 $ builder ->addTool (
88103 handler: [ReportProcessed::class, 'get ' ],
89104 name: ReportProcessed::TOOL_NAME ,
@@ -107,6 +122,7 @@ public function createServer(): Server
107122 $ builder ->addTool (
108123 [ApiGet::class, 'get ' ],
109124 ApiGet::TOOL_NAME ,
125+ null ,
110126 "Use when: you already know the Matomo API method name and need its exact signature. \n"
111127 . "Purpose: return one authoritative API method summary with parameter metadata. \n"
112128 . "Do not use: for broad discovery across APIs; use " . ApiList::TOOL_NAME . ' instead. ' ,
@@ -124,6 +140,7 @@ public function createServer(): Server
124140 $ builder ->addTool (
125141 [ApiList::class, 'list ' ],
126142 ApiList::TOOL_NAME ,
143+ null ,
127144 "Use when: you need discoverable Matomo API methods and parameter metadata. \n"
128145 . "Purpose: return paginated API method summaries aligned with Matomo API docs visibility. \n"
129146 . "Next: choose a method and map parameters for subsequent raw API tooling. " ,
@@ -161,12 +178,68 @@ public function createServer(): Server
161178 return $ builder ->build ();
162179 }
163180
181+ private function registerAttributeTools (Builder $ builder ): void
182+ {
183+ $ schemaGenerator = new SchemaGenerator (new DocBlockParser ());
184+
185+ $ tools = [
186+ [DimensionGet::class, 'get ' ],
187+ [DimensionList::class, 'list ' ],
188+ [GoalGet::class, 'get ' ],
189+ [GoalList::class, 'list ' ],
190+ [ReportList::class, 'list ' ],
191+ [ReportMetadata::class, 'get ' ],
192+ [SegmentGet::class, 'get ' ],
193+ [SegmentList::class, 'list ' ],
194+ [SiteGet::class, 'get ' ],
195+ [SiteList::class, 'list ' ],
196+ [SiteSearch::class, 'search ' ],
197+ ];
198+
199+ foreach ($ tools as [$ className , $ methodName ]) {
200+ $ this ->registerAttributeTool ($ builder , $ schemaGenerator , $ className , $ methodName );
201+ }
202+ }
203+
204+ /**
205+ * @param class-string $className
206+ */
207+ private function registerAttributeTool (
208+ Builder $ builder ,
209+ SchemaGenerator $ schemaGenerator ,
210+ string $ className ,
211+ string $ methodName ,
212+ ): void {
213+ $ method = new \ReflectionMethod ($ className , $ methodName );
214+ $ attribute = $ method ->getAttributes (McpTool::class, \ReflectionAttribute::IS_INSTANCEOF )[0 ] ?? null ;
215+
216+ if ($ attribute === null ) {
217+ throw new \LogicException (sprintf ('Missing McpTool attribute on %s::%s. ' , $ className , $ methodName ));
218+ }
219+
220+ /** @var McpTool $tool */
221+ $ tool = $ attribute ->newInstance ();
222+
223+ $ builder ->addTool (
224+ [$ className , $ methodName ],
225+ $ tool ->name ,
226+ $ tool ->title ,
227+ $ tool ->description ,
228+ $ tool ->annotations ,
229+ $ schemaGenerator ->generate ($ method ),
230+ $ tool ->icons ,
231+ $ tool ->meta ,
232+ $ schemaGenerator ->generateOutputSchema ($ method ),
233+ );
234+ }
235+
164236 private function registerRawApiCallTools (Builder $ builder , string $ rawApiAccessMode ): void
165237 {
166238 if (RawApiAccessMode::allowsCategory ($ rawApiAccessMode , RawApiAccessMode::READ )) {
167239 $ builder ->addTool (
168240 [ApiCallRead::class, 'call ' ],
169241 ApiCallRead::TOOL_NAME ,
242+ null ,
170243 "Use when: you need to execute a known read-only Matomo API method directly. \n"
171244 . "Purpose: call one allowed read method and return its result plus the resolved method metadata. \n"
172245 . "Next: use " . ApiGet::TOOL_NAME . ' or ' . ApiList::TOOL_NAME
@@ -188,6 +261,7 @@ private function registerRawApiCallTools(Builder $builder, string $rawApiAccessM
188261 $ builder ->addTool (
189262 [ApiCallCreate::class, 'call ' ],
190263 ApiCallCreate::TOOL_NAME ,
264+ null ,
191265 "Use when: you need to execute a known create-style Matomo API method directly. \n"
192266 . "Purpose: call one allowed create method and return its result plus the "
193267 . " resolved method metadata. \n"
@@ -210,6 +284,7 @@ private function registerRawApiCallTools(Builder $builder, string $rawApiAccessM
210284 $ builder ->addTool (
211285 [ApiCallUpdate::class, 'call ' ],
212286 ApiCallUpdate::TOOL_NAME ,
287+ null ,
213288 "Use when: you need to execute a known update-style Matomo API method directly. \n"
214289 . "Purpose: call one allowed update method and return its result plus the "
215290 . " resolved method metadata. \n"
@@ -232,6 +307,7 @@ private function registerRawApiCallTools(Builder $builder, string $rawApiAccessM
232307 $ builder ->addTool (
233308 [ApiCallDelete::class, 'call ' ],
234309 ApiCallDelete::TOOL_NAME ,
310+ null ,
235311 "Use when: you need to execute a known delete-style Matomo API method directly. \n"
236312 . "Purpose: call one allowed delete method and return its result plus the "
237313 . " resolved method metadata. \n"
@@ -254,6 +330,7 @@ private function registerRawApiCallTools(Builder $builder, string $rawApiAccessM
254330 $ builder ->addTool (
255331 [ApiCallFull::class, 'call ' ],
256332 ApiCallFull::TOOL_NAME ,
333+ null ,
257334 "Use when: you need to execute a known Matomo API method directly and "
258335 . " it is not safely covered by one CRUD-specific tool. \n"
259336 . "Purpose: call one allowed full-access API method and return its result "
0 commit comments