Init
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
package mcps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cs-agent/internal/pkg/errorsx"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type Client struct{}
|
||||
|
||||
func NewClient() *Client {
|
||||
return &Client{}
|
||||
}
|
||||
|
||||
func (c *Client) TestConnection(ctx context.Context, cfg ServerConfig) (*ConnectionResult, error) {
|
||||
session, closeFn, err := c.connect(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeFn()
|
||||
|
||||
initResult := session.InitializeResult()
|
||||
serverName := ""
|
||||
version := ""
|
||||
protocol := ""
|
||||
if initResult != nil {
|
||||
serverName = initResult.ServerInfo.Name
|
||||
version = initResult.ServerInfo.Version
|
||||
protocol = initResult.ProtocolVersion
|
||||
}
|
||||
return &ConnectionResult{
|
||||
ServerCode: cfg.Code,
|
||||
Endpoint: cfg.Endpoint,
|
||||
Protocol: protocol,
|
||||
ServerName: serverName,
|
||||
Version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListTools(ctx context.Context, cfg ServerConfig) ([]ToolInfo, error) {
|
||||
session, closeFn, err := c.connect(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeFn()
|
||||
|
||||
result, err := session.ListTools(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("列出 MCP 工具失败: %w", err)
|
||||
}
|
||||
ret := make([]ToolInfo, 0, len(result.Tools))
|
||||
for _, tool := range result.Tools {
|
||||
ret = append(ret, ToolInfo{
|
||||
Name: tool.Name,
|
||||
Title: tool.Title,
|
||||
Description: tool.Description,
|
||||
InputSchema: tool.InputSchema,
|
||||
OutputSchema: tool.OutputSchema,
|
||||
})
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (c *Client) CallTool(ctx context.Context, cfg ServerConfig, toolName string, arguments map[string]any) (*ToolCallResult, error) {
|
||||
toolName = strings.TrimSpace(toolName)
|
||||
if toolName == "" {
|
||||
return nil, errorsx.InvalidParam("toolName不能为空")
|
||||
}
|
||||
|
||||
session, closeFn, err := c.connect(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeFn()
|
||||
|
||||
result, err := session.CallTool(ctx, &mcp.CallToolParams{
|
||||
Name: toolName,
|
||||
Arguments: arguments,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("调用 MCP 工具失败: %w", err)
|
||||
}
|
||||
return &ToolCallResult{
|
||||
ServerCode: cfg.Code,
|
||||
ToolName: toolName,
|
||||
IsError: result.IsError,
|
||||
Content: convertContents(result.Content),
|
||||
StructuredContent: result.StructuredContent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) connect(ctx context.Context, cfg ServerConfig) (*mcp.ClientSession, func(), error) {
|
||||
if strings.TrimSpace(cfg.Code) == "" {
|
||||
return nil, nil, errorsx.InvalidParam("serverCode不能为空")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Endpoint) == "" {
|
||||
return nil, nil, errorsx.InvalidParam("MCP endpoint不能为空")
|
||||
}
|
||||
|
||||
timeout := time.Duration(cfg.TimeoutMS) * time.Millisecond
|
||||
if timeout <= 0 {
|
||||
timeout = 15 * time.Second
|
||||
}
|
||||
connCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &headerRoundTripper{
|
||||
next: http.DefaultTransport,
|
||||
headers: cfg.Headers,
|
||||
},
|
||||
}
|
||||
client := mcp.NewClient(&mcp.Implementation{
|
||||
Name: "cs-agent-mcp-client",
|
||||
Version: "v1",
|
||||
}, nil)
|
||||
transport := &mcp.StreamableClientTransport{
|
||||
Endpoint: cfg.Endpoint,
|
||||
HTTPClient: httpClient,
|
||||
MaxRetries: 0,
|
||||
DisableStandaloneSSE: true,
|
||||
}
|
||||
session, err := client.Connect(connCtx, transport, nil)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, fmt.Errorf("连接 MCP Server 失败: %w", err)
|
||||
}
|
||||
return session, func() {
|
||||
_ = session.Close()
|
||||
cancel()
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertContents(contents []mcp.Content) []ToolResultContent {
|
||||
ret := make([]ToolResultContent, 0, len(contents))
|
||||
for _, item := range contents {
|
||||
switch v := item.(type) {
|
||||
case *mcp.TextContent:
|
||||
ret = append(ret, ToolResultContent{
|
||||
Type: "text",
|
||||
Text: v.Text,
|
||||
})
|
||||
case *mcp.ImageContent:
|
||||
ret = append(ret, ToolResultContent{
|
||||
Type: "image",
|
||||
Data: map[string]any{
|
||||
"mimeType": v.MIMEType,
|
||||
"data": v.Data,
|
||||
},
|
||||
})
|
||||
case *mcp.AudioContent:
|
||||
ret = append(ret, ToolResultContent{
|
||||
Type: "audio",
|
||||
Data: map[string]any{
|
||||
"mimeType": v.MIMEType,
|
||||
"data": v.Data,
|
||||
},
|
||||
})
|
||||
case *mcp.EmbeddedResource:
|
||||
ret = append(ret, ToolResultContent{
|
||||
Type: "resource",
|
||||
Data: v.Resource,
|
||||
})
|
||||
default:
|
||||
ret = append(ret, ToolResultContent{
|
||||
Type: "unknown",
|
||||
Data: v,
|
||||
})
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type headerRoundTripper struct {
|
||||
next http.RoundTripper
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
func (r *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
next := r.next
|
||||
if next == nil {
|
||||
next = http.DefaultTransport
|
||||
}
|
||||
clone := req.Clone(req.Context())
|
||||
for key, value := range r.headers {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
clone.Header.Set(key, value)
|
||||
}
|
||||
return next.RoundTrip(clone)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cs-agent/internal/pkg/config"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type systemToolProvider struct{}
|
||||
|
||||
func NewSystemToolProvider() ToolProvider {
|
||||
return &systemToolProvider{}
|
||||
}
|
||||
|
||||
func (p *systemToolProvider) Name() string {
|
||||
return "system"
|
||||
}
|
||||
|
||||
func (p *systemToolProvider) Register(server *mcp.Server) error {
|
||||
mcp.AddTool(
|
||||
server,
|
||||
&mcp.Tool{
|
||||
Name: "server_time",
|
||||
Description: "获取当前服务端时间,可选传入时区。",
|
||||
},
|
||||
func(_ context.Context, _ *mcp.CallToolRequest, args serverTimeArgs) (*mcp.CallToolResult, map[string]any, error) {
|
||||
loc := time.Local
|
||||
timezone := args.Timezone
|
||||
if timezone == "" {
|
||||
timezone = "Local"
|
||||
} else if loaded, err := time.LoadLocation(timezone); err == nil {
|
||||
loc = loaded
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
return nil, map[string]any{
|
||||
"timezone": timezone,
|
||||
"timestamp": now.Format("2006-01-02 15:04:05"),
|
||||
"unix": now.Unix(),
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
|
||||
mcp.AddTool(
|
||||
server,
|
||||
&mcp.Tool{
|
||||
Name: "service_info",
|
||||
Description: "查看当前 cs-agent 服务的基础运行信息。",
|
||||
},
|
||||
func(ctx context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, map[string]any, error) {
|
||||
cfg := config.Current()
|
||||
return nil, map[string]any{
|
||||
"name": "cs-agent",
|
||||
"version": "v1",
|
||||
"mcpPath": "/api/mcp",
|
||||
"port": cfg.Server.Port,
|
||||
"mcpEnabled": cfg.MCP.Enabled,
|
||||
"vectorDb": cfg.VectorDB.Type,
|
||||
"storageType": cfg.Storage.Default,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
type serverTimeArgs struct {
|
||||
Timezone string `json:"timezone,omitempty" jsonschema:"可选时区名称,例如 Asia/Shanghai 或 UTC"`
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type ToolProvider interface {
|
||||
Name() string
|
||||
Register(server *mcp.Server) error
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package mcps
|
||||
|
||||
import (
|
||||
"cs-agent/internal/ai/mcps/providers"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
func defaultProviders() []providers.ToolProvider {
|
||||
return []providers.ToolProvider{
|
||||
providers.NewSystemToolProvider(),
|
||||
// 在这里注册其他的 ToolProvider
|
||||
}
|
||||
}
|
||||
|
||||
func registerProviders(server *mcp.Server) error {
|
||||
for _, provider := range defaultProviders() {
|
||||
if err := provider.Register(server); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package mcps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"cs-agent/internal/pkg/config"
|
||||
"cs-agent/internal/pkg/errorsx"
|
||||
)
|
||||
|
||||
type RuntimeService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var Runtime = NewRuntimeService()
|
||||
|
||||
func NewRuntimeService() *RuntimeService {
|
||||
return &RuntimeService{
|
||||
client: NewClient(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RuntimeService) CallTool(ctx context.Context, serverCode string, toolName string, arguments map[string]any) (*ToolCallResult, error) {
|
||||
server, err := s.resolveServer(serverCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.client.CallTool(ctx, server, toolName, arguments)
|
||||
}
|
||||
|
||||
func (s *RuntimeService) ListTools(ctx context.Context, serverCode string) ([]ToolInfo, error) {
|
||||
server, err := s.resolveServer(serverCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.client.ListTools(ctx, server)
|
||||
}
|
||||
|
||||
func (s *RuntimeService) resolveServer(serverCode string) (ServerConfig, error) {
|
||||
cfg := config.Current()
|
||||
if !cfg.MCP.Enabled {
|
||||
return ServerConfig{}, errorsx.InvalidParam("MCP未启用")
|
||||
}
|
||||
serverCode = strings.TrimSpace(serverCode)
|
||||
if serverCode == "" {
|
||||
return ServerConfig{}, errorsx.InvalidParam("serverCode不能为空")
|
||||
}
|
||||
server, ok := cfg.MCP.Servers[serverCode]
|
||||
if !ok {
|
||||
return ServerConfig{}, errorsx.InvalidParam("MCP服务配置不存在")
|
||||
}
|
||||
if !server.Enabled {
|
||||
return ServerConfig{}, errorsx.InvalidParam("MCP服务未启用")
|
||||
}
|
||||
return ServerConfig{
|
||||
Code: serverCode,
|
||||
Endpoint: strings.TrimSpace(server.Endpoint),
|
||||
TimeoutMS: server.TimeoutMS,
|
||||
Headers: cloneRuntimeHeaders(server.Headers),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func cloneRuntimeHeaders(headers map[string]string) map[string]string {
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
ret := make(map[string]string, len(headers))
|
||||
for key, value := range headers {
|
||||
ret[key] = value
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package mcps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
func NewHTTPHandler() http.Handler {
|
||||
server := newServer()
|
||||
return mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server {
|
||||
return server
|
||||
}, &mcp.StreamableHTTPOptions{
|
||||
JSONResponse: true,
|
||||
SessionTimeout: 2 * time.Minute,
|
||||
})
|
||||
}
|
||||
|
||||
func newServer() *mcp.Server {
|
||||
server := mcp.NewServer(&mcp.Implementation{
|
||||
Name: "cs-agent-mcp-server",
|
||||
Title: "CS Agent MCP Server",
|
||||
Version: "v1",
|
||||
WebsiteURL: "https://github.com/modelcontextprotocol",
|
||||
}, nil)
|
||||
if err := registerProviders(server); err != nil {
|
||||
panic(fmt.Sprintf("register mcp providers failed: %v", err))
|
||||
}
|
||||
return server
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package mcps
|
||||
|
||||
type ServerConfig struct {
|
||||
Code string
|
||||
Endpoint string
|
||||
TimeoutMS int
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
Code string `json:"code"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
TimeoutMS int `json:"timeoutMs"`
|
||||
}
|
||||
|
||||
type ConnectionResult struct {
|
||||
ServerCode string `json:"serverCode"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Protocol string `json:"protocol"`
|
||||
ServerName string `json:"serverName"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type ToolInfo struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
InputSchema any `json:"inputSchema"`
|
||||
OutputSchema any `json:"outputSchema,omitempty"`
|
||||
}
|
||||
|
||||
type ToolResultContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type ToolCallResult struct {
|
||||
ServerCode string `json:"serverCode"`
|
||||
ToolName string `json:"toolName"`
|
||||
IsError bool `json:"isError"`
|
||||
Content []ToolResultContent `json:"content"`
|
||||
StructuredContent any `json:"structuredContent,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user