mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
docs: 项目概览(OVERVIEW.md) + 插件开发指南(PLUGIN_DEV.md) + MCP 适配器插件
- OVERVIEW.md: 非技术用户友好的项目介绍和目标 - PLUGIN_DEV.md: 完整插件开发指南(含三种开发方式、API 参考、最佳实践) - internal/plugins/mcp/: MCP 协议适配器插件(JSON-RPC over stdio/SSE) - client.go: MCP 客户端(ListTools / CallTool) - stdio.go: 子进程 stdin/stdout 传输 - sse.go: HTTP POST 传输 - plugin.go: init() 自注册 + Settings 配置读取 - README.md 更新链接
This commit is contained in:
147
internal/plugins/mcp/plugin.go
Normal file
147
internal/plugins/mcp/plugin.go
Normal file
@ -0,0 +1,147 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
// MCP 服务器配置(来自 config_mcp 表或 skill.json)
|
||||
type serverConfig struct {
|
||||
Name string `json:"name"`
|
||||
Command string `json:"command,omitempty"` // stdio 模式
|
||||
Args []string `json:"args,omitempty"`
|
||||
Env []string `json:"env,omitempty"`
|
||||
URL string `json:"url,omitempty"` // SSE 模式
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.RegisterFactory("mcp", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
servers []*Server
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func New(name string) *Plugin {
|
||||
return &Plugin{name: name}
|
||||
}
|
||||
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
// 从插件配置读取 MCP 服务器列表
|
||||
cfgs, err := p.loadConfig(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load mcp config: %w", err)
|
||||
}
|
||||
if len(cfgs) == 0 {
|
||||
log.Printf("[mcp] no servers configured, idle")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
server, tools, err := p.connectServer(cfg)
|
||||
if err != nil {
|
||||
log.Printf("[mcp] connect %s: %v", cfg.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
toolName := fmt.Sprintf("%s_%s", cfg.Name, tool.Name)
|
||||
tDef := sdk.ToolDef{
|
||||
Name: toolName,
|
||||
Description: fmt.Sprintf("[MCP/%s] %s", cfg.Name, tool.Description),
|
||||
Parameters: tool.InputSchema,
|
||||
}
|
||||
tHandler := p.makeHandler(server, tool.Name)
|
||||
if err := s.RegisterTool(toolName, tDef, tHandler); err != nil {
|
||||
log.Printf("[mcp] register tool %s: %v", toolName, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[mcp] registered tool: %s (%s)", toolName, cfg.Name)
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
p.servers = append(p.servers, server)
|
||||
p.mu.Unlock()
|
||||
log.Printf("[mcp] connected server: %s (%d tools)", cfg.Name, len(tools))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) loadConfig(s *sdk.PluginSDK) ([]serverConfig, error) {
|
||||
// 优先从 skill.json(config map)读取
|
||||
raw, err := s.Settings().Get("servers")
|
||||
if err == nil {
|
||||
switch v := raw.(type) {
|
||||
case string:
|
||||
var cfgs []serverConfig
|
||||
if err := json.Unmarshal([]byte(v), &cfgs); err == nil && len(cfgs) > 0 {
|
||||
return cfgs, nil
|
||||
}
|
||||
case []interface{}:
|
||||
data, _ := json.Marshal(v)
|
||||
var cfgs []serverConfig
|
||||
if json.Unmarshal(data, &cfgs) == nil && len(cfgs) > 0 {
|
||||
return cfgs, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 备用:从 JSON 文件读取
|
||||
// 没有配置时不报错,只返回空
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) connectServer(cfg serverConfig) (*Server, []MCPTool, error) {
|
||||
var transport Transport
|
||||
|
||||
if cfg.URL != "" {
|
||||
transport = NewSSETransport(cfg.URL)
|
||||
} else if cfg.Command != "" {
|
||||
var err error
|
||||
transport, err = NewStdioTransport(cfg.Command, cfg.Args, cfg.Env)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("stdio transport: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("neither command nor url specified")
|
||||
}
|
||||
|
||||
server := NewServer(cfg.Name, transport)
|
||||
|
||||
tools, err := server.ListTools()
|
||||
if err != nil {
|
||||
transport.Close()
|
||||
return nil, nil, fmt.Errorf("list tools: %w", err)
|
||||
}
|
||||
|
||||
return server, tools, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) makeHandler(server *Server, toolName string) sdk.ToolHandler {
|
||||
return func(args map[string]interface{}) (interface{}, error) {
|
||||
return server.CallTool(toolName, args)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
for _, s := range p.servers {
|
||||
s.Close()
|
||||
}
|
||||
p.servers = nil
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user