Skip to content

Commit 4c234ab

Browse files
localai-botmudler
andauthored
refactor(agents): bump skillserver, drop redundant Name from list_skills output (#9916)
refactor(agents): bump skillserver, drop redundant Name from list_skills/search_skills skillserver's list_skills MCP tool used to ship every entry with name="" (field was commented out), while search_skills populated it - two tools with inconsistent shape for the same data. skill.Name and skill.ID are populated from the same source string anyway (the directory name), so returning both was pure duplication. Bumps github.com/mudler/skillserver to a7317cb, which drops the Name field from both SkillInfo and SearchResult and leaves ID as the single canonical identifier (already what read_skill consumes). Adds core/services/skills/skills_mcp_test.go, a regression that drives the LocalAI FilesystemManager through an in-process MCP session and asserts a newly-created skill is visible by ID on the still-open session. This is a cleanup, not the root cause of #9868 - the reporter likely sees something deeper than a cosmetic JSON shape issue. Assisted-by: Claude:claude-opus-4-7 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent c68818a commit 4c234ab

3 files changed

Lines changed: 120 additions & 1 deletion

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package skills_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"os"
7+
"testing"
8+
"time"
9+
10+
"github.com/modelcontextprotocol/go-sdk/mcp"
11+
agiSkills "github.com/mudler/LocalAGI/services/skills"
12+
localskills "github.com/mudler/LocalAI/core/services/skills"
13+
14+
. "github.com/onsi/ginkgo/v2"
15+
. "github.com/onsi/gomega"
16+
)
17+
18+
func TestSkillsMCP(t *testing.T) {
19+
RegisterFailHandler(Fail)
20+
RunSpecs(t, "Skills MCP test")
21+
}
22+
23+
// listSkillsResult mirrors the output struct of skillserver's list_skills tool.
24+
type listSkillsResult struct {
25+
Skills []struct {
26+
ID string `json:"id"`
27+
Description string `json:"description,omitempty"`
28+
} `json:"skills"`
29+
}
30+
31+
// Exercises the same wire the agent uses at runtime: open an in-process
32+
// MCP session via LocalAGI's skills.Service, create a skill through the
33+
// LocalAI FilesystemManager, then list_skills on the still-open session.
34+
// Guards against regressions in the manager <-> MCP session lifecycle
35+
// (e.g. cached manager not picking up newly-created skills).
36+
var _ = Describe("Skills exposed to agent via MCP", func() {
37+
var (
38+
stateDir string
39+
svc *agiSkills.Service
40+
ctx context.Context
41+
cancel context.CancelFunc
42+
)
43+
44+
BeforeEach(func() {
45+
var err error
46+
stateDir, err = os.MkdirTemp("", "skills-mcp-test")
47+
Expect(err).NotTo(HaveOccurred())
48+
49+
// Create the LocalAGI skills service (this is what AgentPoolService wires
50+
// into LocalAGI's state.NewAgentPool for MCP session exposure).
51+
svc, err = agiSkills.NewService(stateDir)
52+
Expect(err).NotTo(HaveOccurred())
53+
54+
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
55+
})
56+
57+
AfterEach(func() {
58+
cancel()
59+
Expect(os.RemoveAll(stateDir)).To(Succeed())
60+
})
61+
62+
It("returns a skill created after the MCP session was established", func() {
63+
// Open the MCP session first — this is what the agent does at startup
64+
// with EnableSkills=true, before any skill might exist.
65+
session, err := svc.GetMCPSession(ctx)
66+
Expect(err).NotTo(HaveOccurred())
67+
Expect(session).NotTo(BeNil())
68+
69+
res, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "list_skills"})
70+
Expect(err).NotTo(HaveOccurred())
71+
Expect(res.IsError).To(BeFalse())
72+
var initial listSkillsResult
73+
Expect(decodeMCPText(res, &initial)).To(Succeed())
74+
Expect(initial.Skills).To(BeEmpty(), "no skills should exist initially")
75+
76+
// Create a skill via the LocalAI FilesystemManager — same code path the
77+
// /api/agents/skills POST endpoint takes.
78+
mgr := localskills.NewFilesystemManager(svc)
79+
_, err = mgr.Create("talk-like-pirate", "Talk like a pirate", "Speak in pirate-style.", "", "", "", nil)
80+
Expect(err).NotTo(HaveOccurred())
81+
82+
// Re-list via the SAME already-open session: the manager is shared,
83+
// so a freshly-created skill must be visible without re-attaching.
84+
res, err = session.CallTool(ctx, &mcp.CallToolParams{Name: "list_skills"})
85+
Expect(err).NotTo(HaveOccurred())
86+
Expect(res.IsError).To(BeFalse())
87+
88+
var got listSkillsResult
89+
Expect(decodeMCPText(res, &got)).To(Succeed())
90+
91+
ids := make([]string, 0, len(got.Skills))
92+
for _, s := range got.Skills {
93+
ids = append(ids, s.ID)
94+
}
95+
Expect(ids).To(ContainElement("talk-like-pirate"))
96+
})
97+
})
98+
99+
func mcpText(res *mcp.CallToolResult) string {
100+
text := ""
101+
for _, c := range res.Content {
102+
if tc, ok := c.(*mcp.TextContent); ok {
103+
text += tc.Text
104+
}
105+
}
106+
return text
107+
}
108+
109+
func decodeMCPText(res *mcp.CallToolResult, out any) error {
110+
text := mcpText(res)
111+
if text == "" {
112+
return nil
113+
}
114+
return json.Unmarshal([]byte(text), out)
115+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ require (
220220
github.com/mschoch/smat v0.2.0 // indirect
221221
github.com/mudler/LocalAGI v0.0.0-20260508125235-37810d918a87
222222
github.com/mudler/localrecall v0.6.1-0.20260507074622-a7724fef6f81 // indirect
223-
github.com/mudler/skillserver v0.0.6
223+
github.com/mudler/skillserver v0.0.7-0.20260520220837-a7317cbf9145
224224
github.com/olekukonko/tablewriter v0.0.5 // indirect
225225
github.com/oxffaa/gopher-parse-sitemap v0.0.0-20191021113419-005d2eb1def4 // indirect
226226
github.com/philippgille/chromem-go v0.7.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,10 @@ github.com/mudler/memory v0.0.0-20260406210934-424c1ecf2cf8 h1:Ry8RiWy8fZ6Ff4E7d
984984
github.com/mudler/memory v0.0.0-20260406210934-424c1ecf2cf8/go.mod h1:EA8Ashhd56o32qN7ouPKFSRUs/Z+LrRCF4v6R2Oarm8=
985985
github.com/mudler/skillserver v0.0.6 h1:ixz6wUekLdTmbnpAavCkTydDF6UdXAG3ncYufSPK9G0=
986986
github.com/mudler/skillserver v0.0.6/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
987+
github.com/mudler/skillserver v0.0.7-0.20260520212528-3dae7f041b1e h1:ryXE1UEzGhLkDFYuaxJ0fZ6fg4l++TWfMCTJ1E7bYS8=
988+
github.com/mudler/skillserver v0.0.7-0.20260520212528-3dae7f041b1e/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
989+
github.com/mudler/skillserver v0.0.7-0.20260520220837-a7317cbf9145 h1:z59tA3IDYPt71nzH1jpxeaA1LuDw8aZfpTQFNU43Zb8=
990+
github.com/mudler/skillserver v0.0.7-0.20260520220837-a7317cbf9145/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
987991
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025 h1:WFLP5FHInarYGXi6B/Ze204x7Xy6q/I4nCZnWEyPHK0=
988992
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025/go.mod h1:QuIFdRstyGJt+MTTkWY+mtD7U6xwjOR6SwKUjmLZtR4=
989993
github.com/mudler/xlog v0.0.6 h1:3nBV4THK8kY0Y8FDXXvWAnuAJoOyO7EAXteJeAoHUC0=

0 commit comments

Comments
 (0)