Compare commits

...

1 Commits

Author SHA1 Message Date
Ettore Di Giacinto
694088ebfe 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>
2026-05-20 22:10:07 +00:00
3 changed files with 120 additions and 1 deletions

View File

@@ -0,0 +1,115 @@
package skills_test
import (
"context"
"encoding/json"
"os"
"testing"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
agiSkills "github.com/mudler/LocalAGI/services/skills"
localskills "github.com/mudler/LocalAI/core/services/skills"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSkillsMCP(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Skills MCP test")
}
// listSkillsResult mirrors the output struct of skillserver's list_skills tool.
type listSkillsResult struct {
Skills []struct {
ID string `json:"id"`
Description string `json:"description,omitempty"`
} `json:"skills"`
}
// Exercises the same wire the agent uses at runtime: open an in-process
// MCP session via LocalAGI's skills.Service, create a skill through the
// LocalAI FilesystemManager, then list_skills on the still-open session.
// Guards against regressions in the manager <-> MCP session lifecycle
// (e.g. cached manager not picking up newly-created skills).
var _ = Describe("Skills exposed to agent via MCP", func() {
var (
stateDir string
svc *agiSkills.Service
ctx context.Context
cancel context.CancelFunc
)
BeforeEach(func() {
var err error
stateDir, err = os.MkdirTemp("", "skills-mcp-test")
Expect(err).NotTo(HaveOccurred())
// Create the LocalAGI skills service (this is what AgentPoolService wires
// into LocalAGI's state.NewAgentPool for MCP session exposure).
svc, err = agiSkills.NewService(stateDir)
Expect(err).NotTo(HaveOccurred())
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
})
AfterEach(func() {
cancel()
os.RemoveAll(stateDir)
})
It("returns a skill created after the MCP session was established", func() {
// Open the MCP session first — this is what the agent does at startup
// with EnableSkills=true, before any skill might exist.
session, err := svc.GetMCPSession(ctx)
Expect(err).NotTo(HaveOccurred())
Expect(session).NotTo(BeNil())
res, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "list_skills"})
Expect(err).NotTo(HaveOccurred())
Expect(res.IsError).To(BeFalse())
var initial listSkillsResult
Expect(decodeMCPText(res, &initial)).To(Succeed())
Expect(initial.Skills).To(BeEmpty(), "no skills should exist initially")
// Create a skill via the LocalAI FilesystemManager — same code path the
// /api/agents/skills POST endpoint takes.
mgr := localskills.NewFilesystemManager(svc)
_, err = mgr.Create("talk-like-pirate", "Talk like a pirate", "Speak in pirate-style.", "", "", "", nil)
Expect(err).NotTo(HaveOccurred())
// Re-list via the SAME already-open session: the manager is shared,
// so a freshly-created skill must be visible without re-attaching.
res, err = session.CallTool(ctx, &mcp.CallToolParams{Name: "list_skills"})
Expect(err).NotTo(HaveOccurred())
Expect(res.IsError).To(BeFalse())
var got listSkillsResult
Expect(decodeMCPText(res, &got)).To(Succeed())
ids := make([]string, 0, len(got.Skills))
for _, s := range got.Skills {
ids = append(ids, s.ID)
}
Expect(ids).To(ContainElement("talk-like-pirate"))
})
})
func mcpText(res *mcp.CallToolResult) string {
text := ""
for _, c := range res.Content {
if tc, ok := c.(*mcp.TextContent); ok {
text += tc.Text
}
}
return text
}
func decodeMCPText(res *mcp.CallToolResult, out any) error {
text := mcpText(res)
if text == "" {
return nil
}
return json.Unmarshal([]byte(text), out)
}

2
go.mod
View File

@@ -220,7 +220,7 @@ require (
github.com/mschoch/smat v0.2.0 // indirect
github.com/mudler/LocalAGI v0.0.0-20260508125235-37810d918a87
github.com/mudler/localrecall v0.6.1-0.20260507074622-a7724fef6f81 // indirect
github.com/mudler/skillserver v0.0.6
github.com/mudler/skillserver v0.0.7-0.20260520220837-a7317cbf9145
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/oxffaa/gopher-parse-sitemap v0.0.0-20191021113419-005d2eb1def4 // indirect
github.com/philippgille/chromem-go v0.7.0 // indirect

4
go.sum
View File

@@ -984,6 +984,10 @@ github.com/mudler/memory v0.0.0-20260406210934-424c1ecf2cf8 h1:Ry8RiWy8fZ6Ff4E7d
github.com/mudler/memory v0.0.0-20260406210934-424c1ecf2cf8/go.mod h1:EA8Ashhd56o32qN7ouPKFSRUs/Z+LrRCF4v6R2Oarm8=
github.com/mudler/skillserver v0.0.6 h1:ixz6wUekLdTmbnpAavCkTydDF6UdXAG3ncYufSPK9G0=
github.com/mudler/skillserver v0.0.6/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
github.com/mudler/skillserver v0.0.7-0.20260520212528-3dae7f041b1e h1:ryXE1UEzGhLkDFYuaxJ0fZ6fg4l++TWfMCTJ1E7bYS8=
github.com/mudler/skillserver v0.0.7-0.20260520212528-3dae7f041b1e/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
github.com/mudler/skillserver v0.0.7-0.20260520220837-a7317cbf9145 h1:z59tA3IDYPt71nzH1jpxeaA1LuDw8aZfpTQFNU43Zb8=
github.com/mudler/skillserver v0.0.7-0.20260520220837-a7317cbf9145/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025 h1:WFLP5FHInarYGXi6B/Ze204x7Xy6q/I4nCZnWEyPHK0=
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025/go.mod h1:QuIFdRstyGJt+MTTkWY+mtD7U6xwjOR6SwKUjmLZtR4=
github.com/mudler/xlog v0.0.6 h1:3nBV4THK8kY0Y8FDXXvWAnuAJoOyO7EAXteJeAoHUC0=