feat(capability/mcp): MCP 客户端(stdlib JSON-RPC over stdio/SSE)+ Registry 自动注册/Unregister(registry 增 Remove)
This commit is contained in:
1 parent
08223c06a5
commit
488cf9c79c
14 files changed
+2212
No files matched your search
@@ -0,0 +1,117 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Load reads MCP server configuration from the process environment. It returns
|
||||
// every configured server; callers filter on Enabled.
|
||||
func Load() (*Config, error) {
|
||||
return FromEnv(os.Environ())
|
||||
}
|
||||
|
||||
// FromEnv builds a Config from a KEY=value pair slice such as os.Environ() or a
|
||||
// test fixture. Named servers are read from MCP_<NAME>_* for indexes; the
|
||||
// unnumbered MCP_* variables describe a single default server. When N is a
|
||||
// positive index, one server is read per group and scanning stops at the first
|
||||
// group with no NAME/COMMAND/URL set.
|
||||
func FromEnv(pairs []string) (*Config, error) {
|
||||
env := make(map[string]string, len(pairs))
|
||||
for _, p := range pairs {
|
||||
k, v, ok := strings.Cut(p, "=")
|
||||
if ok {
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
return &Config{Servers: fromEnvMap(env)}, nil
|
||||
}
|
||||
|
||||
// FromYAML parses a YAML document describing MCP servers, for example:
|
||||
//
|
||||
// servers:
|
||||
// - name: calc
|
||||
// command: mcp-calc
|
||||
// args: [--stdio]
|
||||
// enabled: true
|
||||
func FromYAML(data []byte) (*Config, error) {
|
||||
var doc struct {
|
||||
Servers []MCPConfig `yaml:"servers"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||
return nil, fmt.Errorf("mcp config: %w", err)
|
||||
}
|
||||
return &Config{Servers: doc.Servers}, nil
|
||||
}
|
||||
|
||||
// fromEnvMap builds the server list from a resolved environment map.
|
||||
func fromEnvMap(env map[string]string) []MCPConfig {
|
||||
var servers []MCPConfig
|
||||
if cfg, ok := readOne(env, "MCP"); ok {
|
||||
servers = append(servers, cfg)
|
||||
}
|
||||
for i := 0; ; i++ {
|
||||
cfg, ok := readOne(env, "MCP_"+strconv.Itoa(i))
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
servers = append(servers, cfg)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
// readOne reads a single MCPConfig from env using the given variable prefix
|
||||
// (e.g. "MCP" or "MCP_0"). It reports ok=false when no identifying field is set.
|
||||
func readOne(env map[string]string, prefix string) (MCPConfig, bool) {
|
||||
p := prefix
|
||||
if p == "" {
|
||||
p = "MCP"
|
||||
}
|
||||
name := env[p+"_NAME"]
|
||||
command := env[p+"_COMMAND"]
|
||||
url := env[p+"_URL"]
|
||||
if name == "" && command == "" && url == "" {
|
||||
return MCPConfig{}, false
|
||||
}
|
||||
return MCPConfig{
|
||||
Name: name,
|
||||
Command: command,
|
||||
Args: splitArgs(env[p+"_ARGS"]),
|
||||
URL: url,
|
||||
Enabled: parseBool(env[p+"_ENABLED"], false),
|
||||
}, true
|
||||
}
|
||||
|
||||
// splitArgs splits a comma-separated args value, trimming spaces and empty
|
||||
// entries.
|
||||
func splitArgs(raw string) []string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if t := strings.TrimSpace(p); t != "" {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// parseBool parses a boolean value, defaulting to def on empty or invalid input.
|
||||
func parseBool(raw string, def bool) bool {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return def
|
||||
}
|
||||
b, err := strconv.ParseBool(strings.ToLower(raw))
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in new issue
Block a user