Files
WeKnora/pluginsdk/pluginapi/mcp.go
wizardchen e84081a4f6 feat(plugin): agent tools served by plugins, with result views
A plugin with code can now serve MCP tools itself: an mcpServers
contribution without mcp.url. Messages travel as plugin calls
(POST /v1/mcp/{id}, JSON-RPC inside the envelope), so tool calls carry the
workspace's context and configuration (OAuth fields swapped for tokens)
and the same authentication as every other call; servers are stateless.

- pluginapi MCP types and helpers (TextResult, StructuredResult,
  ToolError); Go p.Tool, Python @plugin.tool; OpenAPI, conformance
  (initialize + tools/list), and served-manifest checks.
- WeKnora side: internal/mcp takes registered transports; the plugin
  transport calls through the invoker, and plugin-served servers appear
  per workspace as MCP services with transport "plugin". Structured
  content is no longer dropped by the MCP client.
- toolViews on mcpServers map a tool's structuredContent to a result view:
  table, cards, kv, markdown, json, or one of the plugin's pages (mount
  mcpServers/<id>, context {tool, arguments, result}). MCP tool results
  carry the view as display_type plugin_tool_view; the chat renders it
  with PluginToolView and prefers it over the generic MCP call view.
- Jira example: search_issues and get_issue tools on a workspace
  connection (tenant config with x-oauth and x-options), shown as a
  table and as Markdown.
2026-09-26 13:04:46 +08:00

115 lines
3.7 KiB
Go

package pluginapi
import (
"encoding/json"
"fmt"
)
// MCPPath is the MCP server a plugin serves itself: an mcpServers
// contribution without a url. The input is one JSON-RPC 2.0 message of the
// Model Context Protocol, the output its JSON-RPC response. Carrying MCP in
// the envelope gives tool calls the workspace's context and configuration,
// and the same authentication as every other call. Servers are stateless:
// each message stands alone, with no session.
func MCPPath(id string) string { return "/v1/mcp/" + id }
// MCPProtocolVersion is the MCP revision plugins speak.
const MCPProtocolVersion = "2025-06-18"
// JSON-RPC error codes MCP servers answer with.
const (
MCPMethodNotFound = -32601
MCPInvalidParams = -32602
)
// MCPRequest is a JSON-RPC request or notification (no ID).
type MCPRequest struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
// MCPResponse is a JSON-RPC response.
type MCPResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Error *MCPError `json:"error,omitempty"`
}
// MCPError is a JSON-RPC error.
type MCPError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// Tool describes a tool for tools/list.
type Tool struct {
Name string `json:"name"`
Title string `json:"title,omitempty"`
Description string `json:"description"`
// InputSchema is the JSON Schema of the arguments (type: object).
InputSchema json.RawMessage `json:"inputSchema"`
// OutputSchema describes StructuredContent, when the tool returns it.
OutputSchema json.RawMessage `json:"outputSchema,omitempty"`
Annotations *ToolAnnotations `json:"annotations,omitempty"`
}
// ToolAnnotations are hints about a tool's behavior.
type ToolAnnotations struct {
// ReadOnlyHint says the tool changes nothing.
ReadOnlyHint bool `json:"readOnlyHint,omitempty"`
// DestructiveHint says the tool may delete or overwrite.
DestructiveHint *bool `json:"destructiveHint,omitempty"`
}
// ToolCallParams are the params of tools/call.
type ToolCallParams struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments,omitempty"`
}
// ToolResult is what tools/call returns. The model reads Content;
// StructuredContent is data for the tool's result view (toolViews).
type ToolResult struct {
Content []ToolContent `json:"content"`
StructuredContent any `json:"structuredContent,omitempty"`
// IsError marks a failure the model should see and may recover from.
IsError bool `json:"isError,omitempty"`
}
// ToolContent is one content block: text, or an image (base64 Data).
type ToolContent struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Data string `json:"data,omitempty"`
MimeType string `json:"mimeType,omitempty"`
}
// TextResult is a result the model reads as text.
func TextResult(text string) *ToolResult {
return &ToolResult{Content: []ToolContent{{Type: "text", Text: text}}}
}
// StructuredResult returns data for the result view, and text for the model
// (the data as JSON when text is empty, as MCP recommends).
func StructuredResult(data any, text string) *ToolResult {
if text == "" {
b, err := json.Marshal(data)
if err != nil {
text = fmt.Sprint(data)
} else {
text = string(b)
}
}
return &ToolResult{Content: []ToolContent{{Type: "text", Text: text}}, StructuredContent: data}
}
// ToolError is a failed call the model sees.
func ToolError(format string, args ...any) *ToolResult {
r := TextResult(fmt.Sprintf(format, args...))
r.IsError = true
return r
}