1<?php
2
3namespace dokuwiki\plugin\mcp;
4
5use dokuwiki\Remote\JsonRpcServer;
6
7/**
8 * Implementation of the Model Context Protocol (MCP) server
9 *
10 * This assumes the streaming HTTP transport. It's a thin wrapper around the JsonRpcServer
11 */
12class McpServer extends JsonRpcServer
13{
14    /** @inheritdoc */
15    public function call($methodname, $args)
16    {
17        switch ($methodname) {
18            case 'initialize':
19                return $this->mcpInitialize();
20            case 'tools/list':
21                return $this->mcpToolsList();
22            case 'tools/call':
23                return $this->mcpToolsCall($args);
24            case 'ping':
25            case 'notifications/initialized':
26            case 'notifications/cancelled':
27                return $this->mcpNOP($args);
28            default:
29                return parent::call($methodname, $args);
30        }
31    }
32
33    /**
34     * Handle the MCP call `initialize`
35     *
36     * @link https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle#initialization
37     * @return array
38     */
39    protected function mcpInitialize()
40    {
41        global $conf;
42        /** @var \helper_plugin_mcp $helper */
43        $helper = plugin_load('helper', 'mcp');
44        $info = $helper->getInfo();
45
46        return [
47            "protocolVersion" => "2025-03-26",
48            "capabilities" => [
49                // FIXME it might be possible to make pages and media available as resources
50                "tools" => ["listChanged" => false]
51            ],
52            "serverInfo" => [
53                "name" => "DokuWiki MCP",
54                "version" => $info['date'],
55            ],
56            "instructions" => sprintf(
57                "Access and interact with the DokuWiki instance called '%s'.",
58                $conf['title']
59            ),
60        ];
61    }
62
63    /**
64     * Handle the MCP call `tools/list`
65     *
66     * @link https://modelcontextprotocol.io/specification/2025-03-26/server/tools#listing-tools
67     * @return array
68     */
69    protected function mcpToolsList()
70    {
71        return [
72            "tools" => (new SchemaGenerator())->getTools()
73        ];
74    }
75
76    /**
77     * Handle the MCP call `tools/call`
78     *
79     * @link https://modelcontextprotocol.io/specification/2025-03-26/server/tools#calling-tools
80     * @param array $args
81     * @return array
82     */
83    protected function mcpToolsCall($args)
84    {
85        // Some LLMs (e.g. Claude) don't allow underscores in method names, so we replace them with dots.
86        // We have to convert them back to underscores for the actual call.
87        $method = str_replace('_', '.', $args['name']);
88        $params = $args['arguments'];
89        $result = parent::call($method, $params);
90
91        # MCP only supports Text, Image and Audio. Complex types will be returned as JSON.
92        // FIXME: we could support image and audio in the core.getMedia call
93        return [
94            "content" => [
95                [
96                    "type" => "text",
97                    "text" => is_scalar($result) ? (string)$result : json_encode($result, JSON_PRETTY_PRINT)
98                ]
99            ],
100            "isError" => false,
101        ];
102    }
103
104    /**
105     * Handle the MCP calls that only need to be acknowledged, but do not require any response.
106     *
107     * @return object
108     */
109    protected function mcpNOP()
110    {
111        return (object)[];
112    }
113}
114