Add MCP client loading and printing functionality
- Introduced a new `printMCPs` function to display loaded MCP clients, categorizing them into standard and agent clients for better clarity. - Enhanced the `Load` function to include loading MCP clients from the assistants directory, improving the overall MCP management. - Added tests for loading assistant MCPs, ensuring that the functionality works as expected and that agent clients are correctly identified and logged. - Updated model loading to support agent models from assistants, ensuring proper integration and functionality across the system.
This commit is contained in:
parent
311350bfbc
commit
cd5b4df503
5 changed files with 444 additions and 0 deletions
74
cmd/start.go
74
cmd/start.go
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/yaoapp/gou/api"
|
||||
"github.com/yaoapp/gou/connector"
|
||||
"github.com/yaoapp/gou/fs"
|
||||
"github.com/yaoapp/gou/mcp"
|
||||
"github.com/yaoapp/gou/plugin"
|
||||
"github.com/yaoapp/gou/schedule"
|
||||
"github.com/yaoapp/gou/server/http"
|
||||
|
|
@ -143,6 +144,7 @@ var startCmd = &cobra.Command{
|
|||
printSchedules(false)
|
||||
printConnectors(false)
|
||||
printStores(false)
|
||||
printMCPs(false)
|
||||
// printStudio(false, host)
|
||||
|
||||
}
|
||||
|
|
@ -230,6 +232,7 @@ var startCmd = &cobra.Command{
|
|||
printSchedules(true)
|
||||
printConnectors(true)
|
||||
printStores(true)
|
||||
printMCPs(true)
|
||||
}
|
||||
|
||||
// Print the warnings
|
||||
|
|
@ -498,6 +501,77 @@ func printApis(silent bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func printMCPs(silent bool) {
|
||||
clients := mcp.ListClients()
|
||||
if len(clients) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if silent {
|
||||
for _, clientID := range clients {
|
||||
log.Info("[MCP] %s loaded", clientID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Separate agent MCPs from standard MCPs
|
||||
agentClients := []string{}
|
||||
standardClients := []string{}
|
||||
for _, clientID := range clients {
|
||||
if len(clientID) >= 7 && clientID[:7] == "agents." {
|
||||
agentClients = append(agentClients, clientID)
|
||||
} else {
|
||||
standardClients = append(standardClients, clientID)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(color.WhiteString("\n---------------------------------"))
|
||||
fmt.Println(color.WhiteString(L("MCP Clients List (%d)"), len(clients)))
|
||||
fmt.Println(color.WhiteString("---------------------------------"))
|
||||
|
||||
if len(standardClients) > 0 {
|
||||
fmt.Println(color.WhiteString("\n%s (%d)", "Standard MCPs", len(standardClients)))
|
||||
fmt.Println(color.WhiteString("--------------------------"))
|
||||
for _, clientID := range standardClients {
|
||||
mapping, err := mcp.GetClientMapping(clientID)
|
||||
if err != nil {
|
||||
fmt.Print(color.CyanString("[MCP] %s", clientID))
|
||||
fmt.Print(color.WhiteString("\tloaded\n"))
|
||||
continue
|
||||
}
|
||||
|
||||
toolsCount := 0
|
||||
if mapping.Tools != nil {
|
||||
toolsCount = len(mapping.Tools)
|
||||
}
|
||||
|
||||
fmt.Print(color.CyanString("[MCP] %s", clientID))
|
||||
fmt.Print(color.WhiteString("\ttools: %d\n", toolsCount))
|
||||
}
|
||||
}
|
||||
|
||||
if len(agentClients) > 0 {
|
||||
fmt.Println(color.WhiteString("\n%s (%d)", "Agent MCPs", len(agentClients)))
|
||||
fmt.Println(color.WhiteString("--------------------------"))
|
||||
for _, clientID := range agentClients {
|
||||
mapping, err := mcp.GetClientMapping(clientID)
|
||||
if err != nil {
|
||||
fmt.Print(color.CyanString("[MCP] %s", clientID))
|
||||
fmt.Print(color.WhiteString("\tloaded\n"))
|
||||
continue
|
||||
}
|
||||
|
||||
toolsCount := 0
|
||||
if mapping.Tools != nil {
|
||||
toolsCount = len(mapping.Tools)
|
||||
}
|
||||
|
||||
fmt.Print(color.CyanString("[MCP] %s", clientID))
|
||||
fmt.Print(color.WhiteString("\ttools: %d\n", toolsCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func colorMehtod(method string) string {
|
||||
method = strings.ToUpper(method)
|
||||
switch method {
|
||||
|
|
|
|||
106
mcp/mcp.go
106
mcp/mcp.go
|
|
@ -3,6 +3,7 @@ package mcp
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -44,6 +45,14 @@ func Load(cfg config.Config) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Load MCP clients from assistants
|
||||
errsAssistants := loadAssistantMCPs()
|
||||
if len(errsAssistants) > 0 {
|
||||
for _, err := range errsAssistants {
|
||||
log.Error("Load assistant MCP clients error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Load database MCP clients (ignore error)
|
||||
errs := loadDatabaseMCPs()
|
||||
if len(errs) > 0 {
|
||||
|
|
@ -54,6 +63,103 @@ func Load(cfg config.Config) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// loadAssistantMCPs load MCP clients from assistants directory
|
||||
func loadAssistantMCPs() []error {
|
||||
var errs []error = []error{}
|
||||
|
||||
// Check if assistants directory exists
|
||||
exists, err := application.App.Exists("assistants")
|
||||
if err != nil || !exists {
|
||||
log.Trace("Assistants directory not found or not accessible")
|
||||
return errs
|
||||
}
|
||||
|
||||
log.Trace("Loading MCP clients from assistants directory...")
|
||||
|
||||
// Track processed assistants to avoid duplicates
|
||||
processedAssistants := make(map[string]bool)
|
||||
|
||||
// Walk through assistants directory to find all valid assistants with mcps
|
||||
err = application.App.Walk("assistants", func(root, file string, isdir bool) error {
|
||||
if !isdir {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if this is a valid assistant directory (has package.yao)
|
||||
// file is relative path from root, so we need to join root + file
|
||||
pkgFile := filepath.Join(root, file, "package.yao")
|
||||
pkgExists, _ := application.App.Exists(pkgFile)
|
||||
if !pkgExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract assistant ID from path (e.g., "/assistants/expense" -> "expense")
|
||||
// file is like "/tests/mcpload", trim leading "/" and replace "/" with "."
|
||||
assistantID := strings.TrimPrefix(file, "/")
|
||||
assistantID = strings.ReplaceAll(assistantID, "/", ".")
|
||||
|
||||
// Skip if already processed
|
||||
if processedAssistants[assistantID] {
|
||||
return nil
|
||||
}
|
||||
processedAssistants[assistantID] = true
|
||||
|
||||
log.Trace("Found assistant: %s", assistantID)
|
||||
|
||||
// Check if the assistant has an mcps directory
|
||||
mcpsDir := filepath.Join(root, file, "mcps")
|
||||
mcpsDirExists, _ := application.App.Exists(mcpsDir)
|
||||
if !mcpsDirExists {
|
||||
log.Trace("Assistant %s has no mcps directory", assistantID)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Trace("Loading MCPs from assistant %s", assistantID)
|
||||
|
||||
// Load MCP clients from the assistant's mcps directory
|
||||
exts := []string{"*.mcp.yao", "*.mcp.json", "*.mcp.jsonc"}
|
||||
err := application.App.Walk(mcpsDir, func(mcpRoot, mcpFile string, mcpIsDir bool) error {
|
||||
if mcpIsDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate MCP client ID with agents.<assistantID>./ prefix
|
||||
// Support nested paths: "mcps/nested/tool.mcp.yao" -> "nested.tool"
|
||||
relPath := strings.TrimPrefix(mcpFile, mcpsDir+"/")
|
||||
relPath = strings.TrimPrefix(relPath, "/")
|
||||
relPath = strings.TrimSuffix(relPath, ".mcp.yao")
|
||||
relPath = strings.TrimSuffix(relPath, ".mcp.json")
|
||||
relPath = strings.TrimSuffix(relPath, ".mcp.jsonc")
|
||||
mcpName := strings.ReplaceAll(relPath, "/", ".")
|
||||
clientID := fmt.Sprintf("agents.%s.%s", assistantID, mcpName)
|
||||
|
||||
log.Trace("Loading MCP client %s from file %s", clientID, mcpFile)
|
||||
|
||||
_, err := mcp.LoadClient(mcpFile, clientID)
|
||||
if err != nil {
|
||||
log.Error("Failed to load MCP client %s from assistant %s: %s", clientID, assistantID, err.Error())
|
||||
errs = append(errs, fmt.Errorf("failed to load MCP client %s: %w", clientID, err))
|
||||
return nil // Continue loading other MCPs
|
||||
}
|
||||
|
||||
log.Info("Loaded MCP client: %s", clientID)
|
||||
return nil
|
||||
}, exts...)
|
||||
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to walk MCPs in assistant %s: %w", assistantID, err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}, "")
|
||||
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to walk assistants directory: %w", err))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// loadDatabaseMCPs load database MCP clients
|
||||
func loadDatabaseMCPs() []error {
|
||||
var errs []error = []error{}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -127,3 +128,86 @@ func TestUnloadClient(t *testing.T) {
|
|||
// Test that unloading non-existent client doesn't crash
|
||||
mcp.UnloadClient("non_existent")
|
||||
}
|
||||
|
||||
func TestLoadAssistantMCPs(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Load MCPs
|
||||
err := Load(config.Conf)
|
||||
if err != nil {
|
||||
t.Logf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
// List all loaded clients
|
||||
clients := mcp.ListClients()
|
||||
t.Logf("Total loaded MCP clients: %d", len(clients))
|
||||
|
||||
// Filter agent clients
|
||||
agentClients := []string{}
|
||||
for _, id := range clients {
|
||||
if len(id) >= 7 && id[:7] == "agents." {
|
||||
agentClients = append(agentClients, id)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Agent MCP clients: %v", agentClients)
|
||||
|
||||
// Check if the test assistant MCP client is loaded
|
||||
testClientID := "agents.tests.mcpload.test"
|
||||
if mcp.Exists(testClientID) {
|
||||
t.Logf("✓ Test assistant MCP client '%s' loaded successfully", testClientID)
|
||||
|
||||
// Verify we can get the client
|
||||
client, err := mcp.Select(testClientID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, client)
|
||||
|
||||
// Try to list tools
|
||||
ctx := context.Background()
|
||||
toolsResp, err := client.ListTools(ctx, "")
|
||||
if err == nil && toolsResp != nil {
|
||||
t.Logf("✓ Available tools in %s: %d", testClientID, len(toolsResp.Tools))
|
||||
for _, tool := range toolsResp.Tools {
|
||||
t.Logf(" - Tool: %s - %s", tool.Name, tool.Description)
|
||||
}
|
||||
} else {
|
||||
t.Logf("Could not list tools: %v", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
t.Logf("Test assistant MCP client '%s' not found", testClientID)
|
||||
t.Logf("This may be expected if the test assistant is not in the application")
|
||||
}
|
||||
|
||||
// Check for nested MCP client
|
||||
nestedClientID := "agents.tests.mcpload.nested.tool"
|
||||
if mcp.Exists(nestedClientID) {
|
||||
t.Logf("✓ Nested MCP client '%s' loaded successfully", nestedClientID)
|
||||
|
||||
client, err := mcp.Select(nestedClientID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, client)
|
||||
|
||||
ctx := context.Background()
|
||||
toolsResp, err := client.ListTools(ctx, "")
|
||||
if err == nil && toolsResp != nil {
|
||||
t.Logf("✓ Available tools in %s: %d", nestedClientID, len(toolsResp.Tools))
|
||||
for _, tool := range toolsResp.Tools {
|
||||
t.Logf(" - Tool: %s - %s", tool.Name, tool.Description)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Logf("✗ Nested MCP client '%s' not found", nestedClientID)
|
||||
}
|
||||
|
||||
// Report all agent clients found
|
||||
if len(agentClients) > 0 {
|
||||
t.Logf("✓ Successfully loaded %d agent MCP client(s):", len(agentClients))
|
||||
for _, id := range agentClients {
|
||||
t.Logf(" - %s", id)
|
||||
}
|
||||
} else {
|
||||
t.Logf("No agent MCP clients found (this may be expected if no assistants have mcps)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
150
model/model.go
150
model/model.go
|
|
@ -76,6 +76,14 @@ func Load(cfg config.Config) error {
|
|||
return fmt.Errorf("%s", strings.Join(messages, ";\n"))
|
||||
}
|
||||
|
||||
// Load models from assistants
|
||||
errsAssistants := loadAssistantModels()
|
||||
if len(errsAssistants) > 0 {
|
||||
for _, err := range errsAssistants {
|
||||
log.Error("Load assistant models error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Load database models ( ignore error)
|
||||
errs := loadDatabaseModels()
|
||||
if len(errs) > 0 {
|
||||
|
|
@ -131,6 +139,148 @@ func loadSystemModels() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// loadAssistantModels load models from assistants directory
|
||||
func loadAssistantModels() []error {
|
||||
var errs []error = []error{}
|
||||
|
||||
// Check if assistants directory exists
|
||||
exists, err := application.App.Exists("assistants")
|
||||
if err != nil || !exists {
|
||||
log.Trace("Assistants directory not found or not accessible")
|
||||
return errs
|
||||
}
|
||||
|
||||
log.Trace("Loading models from assistants directory...")
|
||||
|
||||
// Track processed assistants to avoid duplicates
|
||||
processedAssistants := make(map[string]bool)
|
||||
|
||||
// Walk through assistants directory to find all valid assistants with models
|
||||
err = application.App.Walk("assistants", func(root, file string, isdir bool) error {
|
||||
if !isdir {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if this is a valid assistant directory (has package.yao)
|
||||
pkgFile := filepath.Join(root, file, "package.yao")
|
||||
pkgExists, _ := application.App.Exists(pkgFile)
|
||||
if !pkgExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract assistant ID from path
|
||||
assistantID := strings.TrimPrefix(file, "/")
|
||||
assistantID = strings.ReplaceAll(assistantID, "/", ".")
|
||||
|
||||
// Skip if already processed
|
||||
if processedAssistants[assistantID] {
|
||||
return nil
|
||||
}
|
||||
processedAssistants[assistantID] = true
|
||||
|
||||
log.Trace("Found assistant: %s", assistantID)
|
||||
|
||||
// Check if the assistant has a models directory
|
||||
modelsDir := filepath.Join(root, file, "models")
|
||||
modelsDirExists, _ := application.App.Exists(modelsDir)
|
||||
if !modelsDirExists {
|
||||
log.Trace("Assistant %s has no models directory", assistantID)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Trace("Loading models from assistant %s", assistantID)
|
||||
|
||||
// Load models from the assistant's models directory
|
||||
exts := []string{"*.mod.yao", "*.mod.json", "*.mod.jsonc"}
|
||||
err := application.App.Walk(modelsDir, func(modelRoot, modelFile string, modelIsDir bool) error {
|
||||
if modelIsDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate model ID with agents.<assistantID>./ prefix
|
||||
// Support nested paths: "models/foo/bar.mod.yao" -> "foo.bar"
|
||||
relPath := strings.TrimPrefix(modelFile, modelsDir+"/")
|
||||
relPath = strings.TrimPrefix(relPath, "/")
|
||||
relPath = strings.TrimSuffix(relPath, ".mod.yao")
|
||||
relPath = strings.TrimSuffix(relPath, ".mod.json")
|
||||
relPath = strings.TrimSuffix(relPath, ".mod.jsonc")
|
||||
modelName := strings.ReplaceAll(relPath, "/", ".")
|
||||
modelID := fmt.Sprintf("agents.%s.%s", assistantID, modelName)
|
||||
|
||||
log.Trace("Loading model %s from file %s", modelID, modelFile)
|
||||
|
||||
// Read and modify model to add table prefix
|
||||
content, err := application.App.Read(modelFile)
|
||||
if err != nil {
|
||||
log.Error("Failed to read model file %s: %s", modelFile, err.Error())
|
||||
errs = append(errs, fmt.Errorf("failed to read model %s: %w", modelID, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse model
|
||||
var modelData map[string]interface{}
|
||||
err = application.Parse(modelFile, content, &modelData)
|
||||
if err != nil {
|
||||
log.Error("Failed to parse model %s: %s", modelID, err.Error())
|
||||
errs = append(errs, fmt.Errorf("failed to parse model %s: %w", modelID, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set table name prefix: agents_<assistantID>_
|
||||
// Convert dots to underscores: tests.mcpload -> agents_tests_mcpload_
|
||||
if table, ok := modelData["table"].(map[string]interface{}); ok {
|
||||
if tableName, ok := table["name"].(string); ok {
|
||||
// Generate prefix from assistant ID
|
||||
prefix := "agents_" + strings.ReplaceAll(assistantID, ".", "_") + "_"
|
||||
|
||||
// Remove any existing prefix if present
|
||||
tableName = strings.TrimPrefix(tableName, "agents_mcpload_")
|
||||
tableName = strings.TrimPrefix(tableName, prefix)
|
||||
|
||||
table["name"] = prefix + tableName
|
||||
content, err = jsoniter.Marshal(modelData)
|
||||
if err != nil {
|
||||
log.Error("Failed to marshal model data for %s: %v", modelID, err)
|
||||
errs = append(errs, fmt.Errorf("failed to marshal model %s: %w", modelID, err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load model with modified content
|
||||
mod, err := model.LoadSource(content, modelID, modelFile)
|
||||
if err != nil {
|
||||
log.Error("Failed to load model %s from assistant %s: %s", modelID, assistantID, err.Error())
|
||||
errs = append(errs, fmt.Errorf("failed to load model %s: %w", modelID, err))
|
||||
return nil // Continue loading other models
|
||||
}
|
||||
|
||||
// Auto migrate the model (like system models)
|
||||
err = mod.Migrate(false, model.WithDonotInsertValues(true))
|
||||
if err != nil {
|
||||
log.Error("Failed to migrate model %s from assistant %s: %s", modelID, assistantID, err.Error())
|
||||
errs = append(errs, fmt.Errorf("failed to migrate model %s: %w", modelID, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Loaded and migrated model: %s", modelID)
|
||||
return nil
|
||||
}, exts...)
|
||||
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to walk models in assistant %s: %w", assistantID, err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}, "")
|
||||
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to walk assistants directory: %w", err))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// LoadDatabaseModels load database models
|
||||
func loadDatabaseModels() []error {
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ func check(t *testing.T) {
|
|||
ids[id] = true
|
||||
}
|
||||
|
||||
// Standard models
|
||||
assert.True(t, ids["user"])
|
||||
assert.True(t, ids["category"])
|
||||
assert.True(t, ids["tag"])
|
||||
|
|
@ -31,4 +32,33 @@ func check(t *testing.T) {
|
|||
assert.True(t, ids["pet.tag"])
|
||||
assert.True(t, ids["user.pet"])
|
||||
assert.True(t, ids["tests.user"])
|
||||
|
||||
// Agent models
|
||||
assert.True(t, ids["agents.tests.mcpload.test_record"], "Agent model agents.tests.mcpload.test_record should be loaded")
|
||||
assert.True(t, ids["agents.tests.mcpload.nested.item"], "Agent nested model agents.tests.mcpload.nested.item should be loaded")
|
||||
|
||||
// Verify table names have correct prefix
|
||||
if testRecordModel, exists := model.Models["agents.tests.mcpload.test_record"]; exists {
|
||||
assert.Equal(t, "agents_tests_mcpload_test_records", testRecordModel.MetaData.Table.Name, "Table name should have agents_tests_mcpload_ prefix")
|
||||
t.Logf("✓ Agent model table name: %s", testRecordModel.MetaData.Table.Name)
|
||||
}
|
||||
|
||||
if nestedItemModel, exists := model.Models["agents.tests.mcpload.nested.item"]; exists {
|
||||
assert.Equal(t, "agents_tests_mcpload_items", nestedItemModel.MetaData.Table.Name, "Nested model table name should have agents_tests_mcpload_ prefix")
|
||||
t.Logf("✓ Nested agent model table name: %s", nestedItemModel.MetaData.Table.Name)
|
||||
}
|
||||
|
||||
// Log all agent models found
|
||||
agentModels := []string{}
|
||||
for id := range model.Models {
|
||||
if len(id) >= 7 && id[:7] == "agents." {
|
||||
agentModels = append(agentModels, id)
|
||||
}
|
||||
}
|
||||
if len(agentModels) > 0 {
|
||||
t.Logf("✓ Found %d agent model(s):", len(agentModels))
|
||||
for _, id := range agentModels {
|
||||
t.Logf(" - %s", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue