Make it working, expose MCP prepare script to UI
Signed-off-by: mudler <mudler@localai.io>
This commit is contained in:
@@ -174,7 +174,15 @@ func (a *Agent) initMCPActions() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MCP STDIO Servers
|
// MCP STDIO Servers
|
||||||
|
|
||||||
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
||||||
|
|
||||||
|
if a.options.mcpPrepareScript != "" {
|
||||||
|
xlog.Debug("Preparing MCP box", "script", a.options.mcpPrepareScript)
|
||||||
|
client := stdio.NewClient(a.options.mcpBoxURL)
|
||||||
|
client.RunProcess(a.context, "/bin/bash", []string{"-c", a.options.mcpPrepareScript}, []string{})
|
||||||
|
}
|
||||||
|
|
||||||
for _, mcpStdioServer := range a.options.mcpStdioServers {
|
for _, mcpStdioServer := range a.options.mcpStdioServers {
|
||||||
client := stdio.NewClient(a.options.mcpBoxURL)
|
client := stdio.NewClient(a.options.mcpBoxURL)
|
||||||
p, err := client.CreateProcess(a.context,
|
p, err := client.CreateProcess(a.context,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ type options struct {
|
|||||||
mcpServers []MCPServer
|
mcpServers []MCPServer
|
||||||
mcpStdioServers []MCPSTDIOServer
|
mcpStdioServers []MCPSTDIOServer
|
||||||
mcpBoxURL string
|
mcpBoxURL string
|
||||||
|
mcpPrepareScript string
|
||||||
newConversationsSubscribers []func(openai.ChatCompletionMessage)
|
newConversationsSubscribers []func(openai.ChatCompletionMessage)
|
||||||
|
|
||||||
observer Observer
|
observer Observer
|
||||||
@@ -223,6 +223,13 @@ func WithMCPBoxURL(url string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithMCPPrepareScript(script string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.mcpPrepareScript = script
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithLLMAPIURL(url string) Option {
|
func WithLLMAPIURL(url string) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.LLMAPI.APIURL = url
|
o.LLMAPI.APIURL = url
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type AgentConfig struct {
|
|||||||
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
|
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
|
||||||
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"`
|
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"`
|
||||||
MCPSTDIOServers []agent.MCPSTDIOServer `json:"mcp_stdio_servers" form:"mcp_stdio_servers"`
|
MCPSTDIOServers []agent.MCPSTDIOServer `json:"mcp_stdio_servers" form:"mcp_stdio_servers"`
|
||||||
|
MCPPrepareScript string `json:"mcp_prepare_script" form:"mcp_prepare_script"`
|
||||||
MCPBoxURL string `json:"mcp_box_url" form:"mcp_box_url"`
|
MCPBoxURL string `json:"mcp_box_url" form:"mcp_box_url"`
|
||||||
|
|
||||||
Description string `json:"description" form:"description"`
|
Description string `json:"description" form:"description"`
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
WithMCPSTDIOServers(config.MCPSTDIOServers...),
|
WithMCPSTDIOServers(config.MCPSTDIOServers...),
|
||||||
WithMCPBoxURL(a.mcpBoxURL),
|
WithMCPBoxURL(a.mcpBoxURL),
|
||||||
WithPrompts(promptBlocks...),
|
WithPrompts(promptBlocks...),
|
||||||
|
WithMCPPrepareScript(config.MCPPrepareScript),
|
||||||
// WithDynamicPrompts(dynamicPrompts...),
|
// WithDynamicPrompts(dynamicPrompts...),
|
||||||
WithCharacter(Character{
|
WithCharacter(Character{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ func (c *Client) CreateProcess(ctx context.Context, command string, args []strin
|
|||||||
|
|
||||||
resp, err := http.Post(url, "application/json", bytes.NewReader(reqBody))
|
resp, err := http.Post(url, "application/json", bytes.NewReader(reqBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
return nil, fmt.Errorf("failed to start process: %w", err)
|
||||||
return nil, fmt.Errorf("failed to start process: %w. body: %s", err, string(body))
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
@@ -259,9 +258,10 @@ type websocketWriter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *websocketWriter) Write(p []byte) (n int, err error) {
|
func (w *websocketWriter) Write(p []byte) (n int, err error) {
|
||||||
err = w.conn.WriteMessage(websocket.TextMessage, p)
|
// Use BinaryMessage type for better compatibility
|
||||||
|
err = w.conn.WriteMessage(websocket.BinaryMessage, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, fmt.Errorf("failed to write WebSocket message: %w", err)
|
||||||
}
|
}
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stdio
|
package stdio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
@@ -11,3 +12,17 @@ func TestSTDIOTransport(t *testing.T) {
|
|||||||
RegisterFailHandler(Fail)
|
RegisterFailHandler(Fail)
|
||||||
RunSpecs(t, "STDIOTransport test suite")
|
RunSpecs(t, "STDIOTransport test suite")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var baseURL string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
baseURL = os.Getenv("STDIO_SERVER_URL")
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = "http://localhost:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = AfterSuite(func() {
|
||||||
|
client := NewClient(baseURL)
|
||||||
|
client.StopGroup("test-group")
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package stdio
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
mcp "github.com/metoro-io/mcp-golang"
|
||||||
|
"github.com/metoro-io/mcp-golang/transport/stdio"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
@@ -12,14 +14,9 @@ import (
|
|||||||
var _ = Describe("Client", func() {
|
var _ = Describe("Client", func() {
|
||||||
var (
|
var (
|
||||||
client *Client
|
client *Client
|
||||||
baseURL string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
baseURL = os.Getenv("STDIO_SERVER_URL")
|
|
||||||
if baseURL == "" {
|
|
||||||
baseURL = "http://localhost:8080"
|
|
||||||
}
|
|
||||||
client = NewClient(baseURL)
|
client = NewClient(baseURL)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -183,5 +180,56 @@ var _ = Describe("Client", func() {
|
|||||||
err = client.StopProcess(process.ID)
|
err = client.StopProcess(process.ID)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("MCP", func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
process, err := client.CreateProcess(ctx,
|
||||||
|
"docker", []string{"run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"},
|
||||||
|
[]string{"GITHUB_PERSONAL_ACCESS_TOKEN=test"}, "test-group")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(process).NotTo(BeNil())
|
||||||
|
Expect(process.ID).NotTo(BeEmpty())
|
||||||
|
|
||||||
|
defer client.StopProcess(process.ID)
|
||||||
|
|
||||||
|
// MCP client
|
||||||
|
|
||||||
|
read, writer, err := client.GetProcessIO(process.ID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(read).NotTo(BeNil())
|
||||||
|
Expect(writer).NotTo(BeNil())
|
||||||
|
|
||||||
|
transport := stdio.NewStdioServerTransportWithIO(read, writer)
|
||||||
|
|
||||||
|
// Create a new client
|
||||||
|
mcpClient := mcp.NewClient(transport)
|
||||||
|
// Initialize the client
|
||||||
|
response, e := mcpClient.Initialize(ctx)
|
||||||
|
Expect(e).NotTo(HaveOccurred())
|
||||||
|
Expect(response).NotTo(BeNil())
|
||||||
|
|
||||||
|
Expect(mcpClient.Ping(ctx)).To(Succeed())
|
||||||
|
|
||||||
|
xlog.Debug("Client initialized: %v", response.Instructions)
|
||||||
|
|
||||||
|
alltools := []mcp.ToolRetType{}
|
||||||
|
var cursor *string
|
||||||
|
for {
|
||||||
|
tools, err := mcpClient.ListTools(ctx, cursor)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(tools).NotTo(BeNil())
|
||||||
|
Expect(tools.Tools).NotTo(BeEmpty())
|
||||||
|
alltools = append(alltools, tools.Tools...)
|
||||||
|
|
||||||
|
if tools.NextCursor == nil {
|
||||||
|
break // No more pages
|
||||||
|
}
|
||||||
|
cursor = tools.NextCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tool := range alltools {
|
||||||
|
xlog.Debug("Tool: %v", tool)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stdio
|
package stdio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -41,9 +40,7 @@ func NewServer() *Server {
|
|||||||
return &Server{
|
return &Server{
|
||||||
processes: make(map[string]*Process),
|
processes: make(map[string]*Process),
|
||||||
groups: make(map[string][]string),
|
groups: make(map[string][]string),
|
||||||
upgrader: websocket.Upgrader{
|
upgrader: websocket.Upgrader{},
|
||||||
CheckOrigin: func(r *http.Request) bool { return true },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +351,7 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Handle stdout and stderr using bufio.Scanner
|
// Handle stdout and stderr
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
select {
|
select {
|
||||||
@@ -365,46 +362,29 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Create a scanner that reads from both stdout and stderr
|
// Create a buffer for reading
|
||||||
scanner := bufio.NewScanner(io.MultiReader(process.Stdout, process.Stderr))
|
buf := make([]byte, 4096)
|
||||||
// Set a larger buffer size for JSON-RPC messages (10MB)
|
reader := io.MultiReader(process.Stdout, process.Stderr)
|
||||||
scanner.Buffer(make([]byte, 10*1024*1024), 10*1024*1024)
|
|
||||||
// Use a custom split function to handle JSON-RPC messages
|
for {
|
||||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
n, err := reader.Read(buf)
|
||||||
if atEOF && len(data) == 0 {
|
if err != nil {
|
||||||
return 0, nil, nil
|
if err != io.EOF {
|
||||||
|
xlog.Debug("Read error", "processID", id, "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for the end of a JSON-RPC message
|
if n > 0 {
|
||||||
for i := 0; i < len(data); i++ {
|
xlog.Debug("Sending message", "processID", id, "size", n)
|
||||||
if data[i] == '\n' {
|
if err := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||||
return i + 1, data[:i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're at EOF, return the remaining data
|
|
||||||
if atEOF {
|
|
||||||
return len(data), data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request more data
|
|
||||||
return 0, nil, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
xlog.Debug("Sending message", "processID", id, "message", line)
|
|
||||||
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
|
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
|
||||||
xlog.Debug("WebSocket output write error", "processID", id, "error", err)
|
xlog.Debug("WebSocket output write error", "processID", id, "error", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xlog.Debug("Message sent to client", "processID", id, "message", line)
|
xlog.Debug("Message sent to client", "processID", id, "size", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
xlog.Debug("Scanner error", "processID", id, "error", err)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user