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