343 lines
8.5 KiB
Go
343 lines
8.5 KiB
Go
package library
|
|
|
|
// DeepSeek Harness has no MCP file: a server is a row of the plugin
|
|
// @deepseek-ai/dsh-mcp-client, one a server, which a patch list (a
|
|
// profile's cordis.patch.yml, or config.yaml before profiles) adds with a
|
|
// top-level insert:
|
|
//
|
|
// - insert: # magpie
|
|
// - id: magpie-mcp-fs
|
|
// name: "@deepseek-ai/dsh-mcp-client"
|
|
// config:
|
|
// serverName: fs
|
|
// transport: stdio
|
|
// command: npx
|
|
//
|
|
// magpie writes each server as an insert of its own, marked; a row the
|
|
// user inserted is read like any other agent's entry, and one by a name
|
|
// magpie writes is taken out of the user's insert for magpie's.
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/yetone/magpie/internal/edit"
|
|
)
|
|
|
|
const dshMCPPlugin = "@deepseek-ai/dsh-mcp-client"
|
|
|
|
const dshMark = "- insert: # magpie"
|
|
|
|
// dshServerName is what dsh-mcp-client takes as a serverName.
|
|
var dshServerName = regexp.MustCompile(`^[A-Za-z0-9_-]{1,32}$`)
|
|
|
|
// errDshName is what the page says of a server whose name dsh can't take.
|
|
var errDshName = errors.New("DeepSeek Harness takes a server name of at most 32 letters, digits, - and _")
|
|
|
|
// dshJS is a value dsh works out when it loads (a !!js expression).
|
|
type dshJS string
|
|
|
|
func hasJS(v any) bool {
|
|
switch x := v.(type) {
|
|
case dshJS:
|
|
return true
|
|
case []any:
|
|
return slices.ContainsFunc(x, hasJS)
|
|
case map[string]any:
|
|
for _, e := range x {
|
|
if hasJS(e) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// dshPatches is a patch list, as its lines: what comes before the first
|
|
// entry, and each entry.
|
|
type dshPatches struct {
|
|
head []string
|
|
items [][]string
|
|
}
|
|
|
|
// dshRow is an mcp-client row an insert adds: its item, and its lines in it.
|
|
type dshRow struct {
|
|
item, from, to int
|
|
alone, magpie bool
|
|
config map[string]any
|
|
}
|
|
|
|
func dshRead(path string) (*dshPatches, error) {
|
|
raw, err := edit.Read(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p := &dshPatches{}
|
|
s := strings.TrimRight(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n")
|
|
if s == "" {
|
|
return p, nil
|
|
}
|
|
for _, l := range strings.Split(s, "\n") {
|
|
t := strings.TrimSpace(l)
|
|
switch {
|
|
case strings.HasPrefix(l, "- ") || l == "-":
|
|
p.items = append(p.items, []string{l})
|
|
case len(p.items) == 0:
|
|
if t != "" && !strings.HasPrefix(t, "#") && t != "[]" {
|
|
return nil, fmt.Errorf("%s is not a list of entries magpie can edit", path)
|
|
}
|
|
if t != "[]" {
|
|
p.head = append(p.head, l)
|
|
}
|
|
case t != "" && !strings.HasPrefix(t, "#") && !strings.HasPrefix(l, " "):
|
|
return nil, fmt.Errorf("%s is not a list of entries magpie can edit", path)
|
|
default:
|
|
p.items[len(p.items)-1] = append(p.items[len(p.items)-1], l)
|
|
}
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (p *dshPatches) write(path string) error {
|
|
out := append([]string{}, p.head...)
|
|
for _, it := range p.items {
|
|
out = append(out, it...)
|
|
}
|
|
if len(p.items) == 0 {
|
|
if _, err := os.Stat(path); err != nil {
|
|
return nil
|
|
}
|
|
out = append(out, "[]") // dsh wants a list, even an empty one
|
|
}
|
|
return edit.WriteAtomic(path, []byte(strings.Join(out, "\n")+"\n"))
|
|
}
|
|
|
|
func mapKey(m *yaml.Node, k string) *yaml.Node {
|
|
if m == nil || m.Kind != yaml.MappingNode {
|
|
return nil
|
|
}
|
|
for i := 0; i+1 < len(m.Content); i += 2 {
|
|
if m.Content[i].Value == k {
|
|
return m.Content[i+1]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// dshValue is the node as Go values, a !!js expression as a dshJS.
|
|
func dshValue(n *yaml.Node) any {
|
|
switch n.Kind {
|
|
case yaml.MappingNode:
|
|
m := map[string]any{}
|
|
for i := 0; i+1 < len(n.Content); i += 2 {
|
|
m[n.Content[i].Value] = dshValue(n.Content[i+1])
|
|
}
|
|
return m
|
|
case yaml.SequenceNode:
|
|
a := []any{}
|
|
for _, c := range n.Content {
|
|
a = append(a, dshValue(c))
|
|
}
|
|
return a
|
|
case yaml.ScalarNode:
|
|
if n.Tag == "!!js" || n.Tag == "tag:yaml.org,2002:js" {
|
|
return dshJS(n.Value)
|
|
}
|
|
}
|
|
var v any
|
|
n.Decode(&v)
|
|
return v
|
|
}
|
|
|
|
// rows are the mcp-client rows of the list's top-level inserts, by
|
|
// serverName.
|
|
func (p *dshPatches) rows() map[string]dshRow {
|
|
out := map[string]dshRow{}
|
|
for i, it := range p.items {
|
|
var doc yaml.Node
|
|
if yaml.Unmarshal([]byte(strings.Join(it, "\n")), &doc) != nil || len(doc.Content) == 0 {
|
|
continue
|
|
}
|
|
seq := doc.Content[0]
|
|
if seq.Kind != yaml.SequenceNode || len(seq.Content) != 1 {
|
|
continue
|
|
}
|
|
patch := seq.Content[0]
|
|
ins := mapKey(patch, "insert")
|
|
if ins == nil || ins.Kind != yaml.SequenceNode || mapKey(patch, "id") != nil {
|
|
continue
|
|
}
|
|
for j, r := range ins.Content {
|
|
name, cfg := mapKey(r, "name"), mapKey(r, "config")
|
|
if name == nil || name.Value != dshMCPPlugin || cfg == nil || cfg.Kind != yaml.MappingNode {
|
|
continue
|
|
}
|
|
sn := mapKey(cfg, "serverName")
|
|
if sn == nil || sn.Value == "" {
|
|
continue
|
|
}
|
|
row := dshRow{item: i, from: r.Line - 1, to: len(it), alone: len(ins.Content) == 1,
|
|
magpie: strings.TrimSpace(it[0]) == dshMark}
|
|
if j+1 < len(ins.Content) {
|
|
row.to = ins.Content[j+1].Line - 1
|
|
}
|
|
// a row that starts on the insert's line or shares one is written
|
|
// flow style: only taken out whole
|
|
if !row.alone && (row.from <= 0 || row.to <= row.from) {
|
|
row.from, row.to = -1, -1
|
|
}
|
|
row.config, _ = dshValue(cfg).(map[string]any)
|
|
out[sn.Value] = row
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// take takes the row out: its insert with it when it was the only one.
|
|
func (p *dshPatches) take(r dshRow, name string) error {
|
|
switch {
|
|
case r.alone:
|
|
p.items = slices.Delete(p.items, r.item, r.item+1)
|
|
case r.from < 0:
|
|
return fmt.Errorf("the insert with DeepSeek Harness's server %s is written on one line; take it out there", name)
|
|
default:
|
|
p.items[r.item] = slices.Delete(p.items[r.item], r.from, r.to)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dshEntries(path string) (map[string]map[string]any, error) {
|
|
out := map[string]map[string]any{}
|
|
p, err := dshRead(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for name, r := range p.rows() {
|
|
if r.config != nil {
|
|
out[name] = r.config
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// dshPut writes the server as magpie's insert, in place of magpie's one
|
|
// by that name, or of the user's row, which leaves the user's insert.
|
|
func dshPut(path, name string, o ordered) error {
|
|
p, err := dshRead(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lines := []string{
|
|
dshMark,
|
|
" - id: magpie-mcp-" + name,
|
|
" name: " + dshScalar(dshMCPPlugin),
|
|
" config:",
|
|
}
|
|
for _, e := range o {
|
|
lines = append(lines, dshYAML(" ", e.k, e.v)...)
|
|
}
|
|
r, ok := p.rows()[name]
|
|
switch {
|
|
case ok && r.magpie && r.alone:
|
|
p.items[r.item] = lines
|
|
case ok:
|
|
if err := p.take(r, name); err != nil {
|
|
return err
|
|
}
|
|
fallthrough
|
|
default:
|
|
p.items = append(p.items, lines)
|
|
}
|
|
return p.write(path)
|
|
}
|
|
|
|
func dshDel(path, name string) error {
|
|
p, err := dshRead(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r, ok := p.rows()[name]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if err := p.take(r, name); err != nil {
|
|
return err
|
|
}
|
|
return p.write(path)
|
|
}
|
|
|
|
// dshScalar is a string as YAML reads it back: JSON's quoting, which YAML
|
|
// shares.
|
|
func dshScalar(s string) string {
|
|
var b bytes.Buffer
|
|
enc := json.NewEncoder(&b)
|
|
enc.SetEscapeHTML(false)
|
|
enc.Encode(s)
|
|
return strings.TrimSpace(b.String())
|
|
}
|
|
|
|
var dshPlain = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_-]*$`)
|
|
|
|
// dshKey is a key bare where YAML reads it so, quoted where not.
|
|
func dshKey(k string) string {
|
|
if dshPlain.MatchString(k) && !slices.Contains([]string{"true", "false", "null", "yes", "no", "on", "off", "y", "n"}, strings.ToLower(k)) {
|
|
return k
|
|
}
|
|
return dshScalar(k)
|
|
}
|
|
|
|
// dshYAML is k: v at the indent, in block style so that a !!js value the
|
|
// user gave can be written back as one.
|
|
func dshYAML(indent, k string, v any) []string {
|
|
key := indent + dshKey(k) + ":"
|
|
switch x := v.(type) {
|
|
case dshJS:
|
|
return []string{key + " !!js " + dshScalar(string(x))}
|
|
case string:
|
|
return []string{key + " " + dshScalar(x)}
|
|
case map[string]string:
|
|
m := map[string]any{}
|
|
for k, s := range x {
|
|
m[k] = s
|
|
}
|
|
return dshYAML(indent, k, m)
|
|
case map[string]any:
|
|
if len(x) == 0 {
|
|
return []string{key + " {}"}
|
|
}
|
|
out := []string{key}
|
|
for _, sub := range slices.Sorted(maps.Keys(x)) {
|
|
out = append(out, dshYAML(indent+" ", sub, x[sub])...)
|
|
}
|
|
return out
|
|
case []any:
|
|
if !hasJS(x) {
|
|
break
|
|
}
|
|
out := []string{key}
|
|
for _, e := range x {
|
|
if js, ok := e.(dshJS); ok {
|
|
out = append(out, indent+" - !!js "+dshScalar(string(js)))
|
|
} else {
|
|
b, _ := json.Marshal(e)
|
|
out = append(out, indent+" - "+string(b))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
var b bytes.Buffer
|
|
enc := json.NewEncoder(&b)
|
|
enc.SetEscapeHTML(false)
|
|
enc.Encode(v)
|
|
return []string{key + " " + strings.TrimSpace(b.String())}
|
|
}
|