mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-20 22:58:34 -04:00
Compare commits
1 Commits
master
...
worktree-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
694088ebfe |
115
core/services/skills/skills_mcp_test.go
Normal file
115
core/services/skills/skills_mcp_test.go
Normal 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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user