1<?php 2 3namespace dokuwiki\plugin\mcp; 4 5use dokuwiki\Remote\ApiCall; 6use dokuwiki\Remote\OpenApiDoc\OpenAPIGenerator; 7 8/** 9 * Generate the JSON schema for MCP tools descriptions 10 * 11 * This is a thin wrapper around the OpenAPIGenerator 12 */ 13class SchemaGenerator extends OpenAPIGenerator 14{ 15 /** 16 * Category marking the deprecated legacy XML-RPC API methods. Tools for these 17 * are not exposed because the modern core.* methods supersede them. 18 * 19 * @var string 20 */ 21 protected const LEGACY_CATEGORY = 'legacy'; 22 23 /** 24 * Method names (without their category prefix) that only read data and never 25 * modify the wiki. Their tools are annotated as read-only so clients may run 26 * them without asking the user for confirmation. 27 * 28 * @var string[] 29 */ 30 protected const READ_ONLY = [ 31 'getAPIVersion', 'getWikiVersion', 'getWikiTitle', 'getWikiTime', 32 'whoAmI', 'aclCheck', 33 'listPages', 'searchPages', 'getRecentPageChanges', 34 'getPage', 'getPageHTML', 'getPageInfo', 'getPageHistory', 35 'getPageLinks', 'getPageBackLinks', 36 'listMedia', 'getRecentMediaChanges', 'getMedia', 'getMediaInfo', 37 'getMediaUsage', 'getMediaHistory', 38 ]; 39 40 /** 41 * Method names (without their category prefix) that modify the wiki but only in 42 * an additive or reversible way, without destroying existing content. Any 43 * modifying method not listed here is treated as destructive. 44 * 45 * @var string[] 46 */ 47 protected const NON_DESTRUCTIVE = [ 48 'appendPage', 'lockPages', 'unlockPages', 'login', 'logoff', 49 ]; 50 51 /** 52 * Get the list of available API calls as tools 53 * 54 * @return array 55 */ 56 public function getTools() 57 { 58 $tools = []; 59 60 $methods = $this->api->getMethods(); 61 62 63 $nullSchema = [ 64 "type" => "object", 65 "properties" => (object)[], 66 "required" => [] 67 ]; 68 69 foreach ($methods as $method => $call) { 70 // skip the deprecated legacy XML-RPC API; the modern core.* methods replace it 71 if ($call->getCategory() === self::LEGACY_CATEGORY) continue; 72 73 $args = $call->getArgs(); 74 75 // Some LLMs (e.g. Claude) don't allow underscores in method names, so we replace them with dots. 76 $tools[] = [ 77 'name' => str_replace('.', '_', $method), 78 'description' => $call->getDescription(), 79 'inputSchema' => $args ? $this->getMethodArguments($args)['schema'] : $nullSchema, 80 'annotations' => $this->getAnnotations($method, $call), 81 ]; 82 } 83 84 return $tools; 85 } 86 87 /** 88 * Build the MCP tool annotations describing the safety of a method call. 89 * 90 * Read-only methods are hinted as such so clients may skip user confirmation. 91 * Modifying methods are marked destructive unless known to be additive or 92 * reversible. Unknown methods default to destructive. 93 * 94 * @param string $method The full API method name including its category prefix 95 * @param ApiCall $call The API call definition 96 * @return array 97 */ 98 protected function getAnnotations($method, ApiCall $call) 99 { 100 $pos = strrpos($method, '.'); 101 $name = $pos === false ? $method : substr($method, $pos + 1); 102 103 $summary = (string)$call->getSummary(); 104 $annotations = [ 105 'title' => $summary !== '' ? $summary : str_replace('.', '_', $method), 106 ]; 107 108 if (in_array($name, self::READ_ONLY, true)) { 109 $annotations['readOnlyHint'] = true; 110 } else { 111 $annotations['readOnlyHint'] = false; 112 $annotations['destructiveHint'] = !in_array($name, self::NON_DESTRUCTIVE, true); 113 } 114 115 return $annotations; 116 } 117} 118