Files
Yingyi / 颖逸 0502614d59 🎨 Add MCP tool management API for kernel plugin (#17834)
* 🎨 Enhance MCP tool management with registration, unregistration, and sanitization features

* 🎨 Enhance MCP tool management with registration, unregistration, and sanitization features

* 🎨 Improve registerTool function with enhanced argument validation and output schema support

* 🎨 Enhance error handling and output schema in MCP tool registration

* 🎨 Add state check in invokeMcpTool to prevent invocation when plugin is not running
2026-06-09 20:21:25 +08:00

246 lines
5.3 KiB
Go

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package mcp
import (
"github.com/siyuan-note/siyuan/kernel/mcp/tools"
"github.com/siyuan-note/siyuan/kernel/util"
)
var supportedProtocolVersions = map[string]bool{
"2025-06-18": true,
"2024-11-05": true,
"2026-07-28": true,
}
func processRequest(req *JsonRpcRequest, session *Session, protocolVersion string) any {
isNotification := req.ID == nil
switch req.Method {
case "initialize":
if isNotification {
return nil
}
return handleInitialize(req, session)
case "notifications/initialized":
session.ready = true
return nil
case "ping":
if isNotification {
return nil
}
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: map[string]interface{}{},
ID: req.ID,
}
case "tools/list":
if isNotification {
return nil
}
return handleToolsList(req.ID)
case "tools/call":
if isNotification {
return nil
}
return handleToolsCall(req)
default:
if isNotification {
return nil
}
return &JsonRpcErrorResponse{
JsonRpc: "2.0",
Error: RpcError{Code: -32601, Message: "Method not found"},
ID: req.ID,
}
}
}
func processRequest2026(req *JsonRpcRequest) any {
isNotification := req.ID == nil
switch req.Method {
case "server/discover":
if isNotification {
return nil
}
return handleDiscover(req)
case "ping":
if isNotification {
return nil
}
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: map[string]interface{}{},
ID: req.ID,
}
case "tools/list":
if isNotification {
return nil
}
return handleToolsList2026(req.ID)
case "tools/call":
if isNotification {
return nil
}
return handleToolsCall(req)
default:
if isNotification {
return nil
}
return &JsonRpcErrorResponse{
JsonRpc: "2.0",
Error: RpcError{Code: -32601, Message: "Method not found"},
ID: req.ID,
}
}
}
func handleInitialize(req *JsonRpcRequest, session *Session) *JsonRpcResponse {
params, _ := req.Params.(map[string]interface{})
serverVersion := ProtocolVersion
if clientVersion, ok := params["protocolVersion"].(string); ok {
if supportedProtocolVersions[clientVersion] {
serverVersion = clientVersion
}
}
session.initialized = true
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: map[string]interface{}{
"protocolVersion": serverVersion,
"capabilities": ServerCapabilities{
Tools: &ToolsCapability{ListChanged: false},
},
"serverInfo": ServerInfo{
Name: ServerName,
Version: util.Ver,
},
},
ID: req.ID,
}
}
func handleToolsList(id any) *JsonRpcResponse {
toolList := tools.GetAllTools()
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: map[string]interface{}{"tools": toolList},
ID: id,
}
}
func handleToolsCall(req *JsonRpcRequest) any {
params, ok := req.Params.(map[string]interface{})
if !ok {
return &JsonRpcErrorResponse{
JsonRpc: "2.0",
Error: RpcError{Code: -32602, Message: "Invalid params"},
ID: req.ID,
}
}
toolName, _ := params["name"].(string)
if toolName == "" {
return &JsonRpcErrorResponse{
JsonRpc: "2.0",
Error: RpcError{Code: -32602, Message: "Invalid params: name is required"},
ID: req.ID,
}
}
t := tools.LookupTool(toolName)
if t == nil {
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: tools.CallToolResult{
Content: []tools.ContentItem{{Type: "text", Text: "tool not found: " + toolName}},
IsError: true,
},
ID: req.ID,
}
}
toolArgs, _ := params["arguments"].(map[string]interface{})
if toolArgs == nil {
toolArgs = map[string]interface{}{}
}
result, err := t.Handler(toolArgs)
if err != nil {
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: tools.CallToolResult{
Content: []tools.ContentItem{{Type: "text", Text: err.Error()}},
IsError: true,
},
ID: req.ID,
}
}
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: result,
ID: req.ID,
}
}
func handleDiscover(req *JsonRpcRequest) *JsonRpcResponse {
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: map[string]interface{}{
"protocolVersion": ProtocolV20260728,
"capabilities": ServerCapabilities{
Tools: &ToolsCapability{ListChanged: false},
},
"serverInfo": ServerInfo{
Name: ServerName,
Version: util.Ver,
},
},
ID: req.ID,
}
}
func handleToolsList2026(id any) *JsonRpcResponse {
toolList := make([]*tools.Tool, 0, len(tools.Registry))
for _, t := range tools.Registry {
toolList = append(toolList, t)
}
return &JsonRpcResponse{
JsonRpc: "2.0",
Result: map[string]interface{}{
"tools": toolList,
"ttlMs": 60000,
"cacheScope": "user",
},
ID: id,
}
}