Merge pull request #1002 from trheyi/main
Update go.mod and go.sum to include new indirect dependencies
This commit is contained in:
commit
1dda938f74
5 changed files with 799 additions and 3 deletions
325
dsl/mcp/cases_test.go
Normal file
325
dsl/mcp/cases_test.go
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/yaoapp/gou/application"
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/kun/log"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/data"
|
||||||
|
"github.com/yaoapp/yao/dsl/types"
|
||||||
|
"github.com/yaoapp/yao/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
// systemModels system models
|
||||||
|
var systemModels = map[string]string{
|
||||||
|
"__yao.dsl": "yao/models/dsl.mod.yao",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// Setup
|
||||||
|
test.Prepare(&testing.T{}, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Load system models
|
||||||
|
model.WithCrypt([]byte(fmt.Sprintf(`{"key":"%s"}`, config.Conf.DB.AESKey)), "AES")
|
||||||
|
model.WithCrypt([]byte(`{}`), "PASSWORD")
|
||||||
|
err := loadSystemModels()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Load system models error: %s", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load application
|
||||||
|
root := os.Getenv("GOU_TEST_APPLICATION")
|
||||||
|
app, err := application.OpenFromDisk(root) // Load app
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Load application error: %s", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
application.Load(app)
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
code := m.Run()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadSystemModels load system models
|
||||||
|
func loadSystemModels() error {
|
||||||
|
for id, path := range systemModels {
|
||||||
|
content, err := data.Read(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse model
|
||||||
|
var data map[string]interface{}
|
||||||
|
err = application.Parse(path, content, &data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set prefix
|
||||||
|
if table, ok := data["table"].(map[string]interface{}); ok {
|
||||||
|
if name, ok := table["name"].(string); ok {
|
||||||
|
table["name"] = "__yao_" + name
|
||||||
|
content, err = jsoniter.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to marshal model data: %v", err)
|
||||||
|
return fmt.Errorf("failed to marshal model data: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Model
|
||||||
|
mod, err := model.LoadSource(content, id, path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("load system model %s error: %s", id, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop table first
|
||||||
|
err = mod.DropTable()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("drop table error: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto migrate
|
||||||
|
err = mod.Migrate(false, model.WithDonotInsertValues(true))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("migrate system model %s error: %s", id, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCase defines a single test case
|
||||||
|
type TestCase struct {
|
||||||
|
ID string
|
||||||
|
Source string
|
||||||
|
UpdatedSource string
|
||||||
|
Tags []string
|
||||||
|
Label string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestCase creates a new test case
|
||||||
|
func NewTestCase() *TestCase {
|
||||||
|
id := getTestID()
|
||||||
|
return &TestCase{
|
||||||
|
ID: id,
|
||||||
|
Source: fmt.Sprintf(`{
|
||||||
|
"name": "Test MCP Client %s",
|
||||||
|
"label": "Test MCP Client",
|
||||||
|
"description": "Test MCP Client Description",
|
||||||
|
"tags": ["test_%s"],
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "echo",
|
||||||
|
"arguments": ["hello", "world"],
|
||||||
|
"env": {
|
||||||
|
"MCP_TEST": "true"
|
||||||
|
},
|
||||||
|
"enable_sampling": true,
|
||||||
|
"enable_roots": false,
|
||||||
|
"timeout": "30s"
|
||||||
|
}`, id, id),
|
||||||
|
UpdatedSource: fmt.Sprintf(`{
|
||||||
|
"name": "Updated MCP Client %s",
|
||||||
|
"label": "Updated MCP Client",
|
||||||
|
"description": "Updated MCP Client Description",
|
||||||
|
"tags": ["test_%s", "updated"],
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "echo",
|
||||||
|
"arguments": ["hello", "updated"],
|
||||||
|
"env": {
|
||||||
|
"MCP_TEST": "true",
|
||||||
|
"MCP_UPDATED": "true"
|
||||||
|
},
|
||||||
|
"enable_sampling": false,
|
||||||
|
"enable_roots": true,
|
||||||
|
"timeout": "60s"
|
||||||
|
}`, id, id),
|
||||||
|
Tags: []string{fmt.Sprintf("test_%s", id)},
|
||||||
|
Label: "Test MCP Client",
|
||||||
|
Description: "Test MCP Client Description",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPTestCase creates a new HTTP test case
|
||||||
|
func NewHTTPTestCase() *TestCase {
|
||||||
|
id := getTestID()
|
||||||
|
return &TestCase{
|
||||||
|
ID: id,
|
||||||
|
Source: fmt.Sprintf(`{
|
||||||
|
"name": "Test HTTP MCP Client %s",
|
||||||
|
"label": "Test HTTP MCP Client",
|
||||||
|
"description": "Test HTTP MCP Client Description",
|
||||||
|
"tags": ["test_%s", "http"],
|
||||||
|
"transport": "http",
|
||||||
|
"url": "http://localhost:8080/mcp",
|
||||||
|
"authorization_token": "Bearer test-token",
|
||||||
|
"enable_sampling": true,
|
||||||
|
"enable_roots": true,
|
||||||
|
"timeout": "30s"
|
||||||
|
}`, id, id),
|
||||||
|
UpdatedSource: fmt.Sprintf(`{
|
||||||
|
"name": "Updated HTTP MCP Client %s",
|
||||||
|
"label": "Updated HTTP MCP Client",
|
||||||
|
"description": "Updated HTTP MCP Client Description",
|
||||||
|
"tags": ["test_%s", "http", "updated"],
|
||||||
|
"transport": "http",
|
||||||
|
"url": "http://localhost:8080/mcp/v2",
|
||||||
|
"authorization_token": "Bearer updated-token",
|
||||||
|
"enable_sampling": false,
|
||||||
|
"enable_roots": false,
|
||||||
|
"timeout": "60s"
|
||||||
|
}`, id, id),
|
||||||
|
Tags: []string{fmt.Sprintf("test_%s", id), "http"},
|
||||||
|
Label: "Test HTTP MCP Client",
|
||||||
|
Description: "Test HTTP MCP Client Description",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSSETestCase creates a new SSE test case
|
||||||
|
func NewSSETestCase() *TestCase {
|
||||||
|
id := getTestID()
|
||||||
|
return &TestCase{
|
||||||
|
ID: id,
|
||||||
|
Source: fmt.Sprintf(`{
|
||||||
|
"name": "Test SSE MCP Client %s",
|
||||||
|
"label": "Test SSE MCP Client",
|
||||||
|
"description": "Test SSE MCP Client Description",
|
||||||
|
"tags": ["test_%s", "sse"],
|
||||||
|
"transport": "sse",
|
||||||
|
"url": "http://localhost:8080/sse",
|
||||||
|
"authorization_token": "Bearer sse-token",
|
||||||
|
"enable_sampling": true,
|
||||||
|
"enable_elicitation": true,
|
||||||
|
"timeout": "45s"
|
||||||
|
}`, id, id),
|
||||||
|
UpdatedSource: fmt.Sprintf(`{
|
||||||
|
"name": "Updated SSE MCP Client %s",
|
||||||
|
"label": "Updated SSE MCP Client",
|
||||||
|
"description": "Updated SSE MCP Client Description",
|
||||||
|
"tags": ["test_%s", "sse", "updated"],
|
||||||
|
"transport": "sse",
|
||||||
|
"url": "http://localhost:8080/sse/v2",
|
||||||
|
"authorization_token": "Bearer updated-sse-token",
|
||||||
|
"enable_sampling": false,
|
||||||
|
"enable_elicitation": false,
|
||||||
|
"timeout": "90s"
|
||||||
|
}`, id, id),
|
||||||
|
Tags: []string{fmt.Sprintf("test_%s", id), "sse"},
|
||||||
|
Label: "Test SSE MCP Client",
|
||||||
|
Description: "Test SSE MCP Client Description",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTestID generates a unique test ID
|
||||||
|
func getTestID() string {
|
||||||
|
return fmt.Sprintf("test_%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOptions returns creation options
|
||||||
|
func (tc *TestCase) CreateOptions() *types.CreateOptions {
|
||||||
|
return &types.CreateOptions{
|
||||||
|
ID: tc.ID,
|
||||||
|
Source: tc.Source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadOptions returns load options
|
||||||
|
func (tc *TestCase) LoadOptions() *types.LoadOptions {
|
||||||
|
return &types.LoadOptions{
|
||||||
|
ID: tc.ID,
|
||||||
|
Source: tc.Source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnloadOptions returns unload options
|
||||||
|
func (tc *TestCase) UnloadOptions() *types.UnloadOptions {
|
||||||
|
return &types.UnloadOptions{
|
||||||
|
ID: tc.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReloadOptions returns reload options
|
||||||
|
func (tc *TestCase) ReloadOptions() *types.ReloadOptions {
|
||||||
|
return &types.ReloadOptions{
|
||||||
|
ID: tc.ID,
|
||||||
|
Source: tc.UpdatedSource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertInfo verifies if the information is correct
|
||||||
|
func (tc *TestCase) AssertInfo(info *types.Info) bool {
|
||||||
|
if info == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.ID == tc.ID &&
|
||||||
|
info.Type == types.TypeMCPClient &&
|
||||||
|
info.Label == tc.Label &&
|
||||||
|
len(info.Tags) == len(tc.Tags) &&
|
||||||
|
info.Description == tc.Description &&
|
||||||
|
!info.Readonly &&
|
||||||
|
!info.Builtin &&
|
||||||
|
!info.Mtime.IsZero() &&
|
||||||
|
!info.Ctime.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertUpdatedInfo verifies if the updated information is correct
|
||||||
|
func (tc *TestCase) AssertUpdatedInfo(info *types.Info) bool {
|
||||||
|
if info == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expectedTags := append(tc.Tags, "updated")
|
||||||
|
return info.ID == tc.ID &&
|
||||||
|
info.Type == types.TypeMCPClient &&
|
||||||
|
info.Label == "Updated MCP Client" &&
|
||||||
|
len(info.Tags) == len(expectedTags) &&
|
||||||
|
info.Description == "Updated MCP Client Description" &&
|
||||||
|
!info.Readonly &&
|
||||||
|
!info.Builtin &&
|
||||||
|
!info.Mtime.IsZero() &&
|
||||||
|
!info.Ctime.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertHTTPInfo verifies if the HTTP client information is correct
|
||||||
|
func (tc *TestCase) AssertHTTPInfo(info *types.Info) bool {
|
||||||
|
if info == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.ID == tc.ID &&
|
||||||
|
info.Type == types.TypeMCPClient &&
|
||||||
|
info.Label == "Test HTTP MCP Client" &&
|
||||||
|
len(info.Tags) == 2 && // test_xxx and http
|
||||||
|
info.Description == "Test HTTP MCP Client Description" &&
|
||||||
|
!info.Readonly &&
|
||||||
|
!info.Builtin &&
|
||||||
|
!info.Mtime.IsZero() &&
|
||||||
|
!info.Ctime.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertSSEInfo verifies if the SSE client information is correct
|
||||||
|
func (tc *TestCase) AssertSSEInfo(info *types.Info) bool {
|
||||||
|
if info == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.ID == tc.ID &&
|
||||||
|
info.Type == types.TypeMCPClient &&
|
||||||
|
info.Label == "Test SSE MCP Client" &&
|
||||||
|
len(info.Tags) == 2 && // test_xxx and sse
|
||||||
|
info.Description == "Test SSE MCP Client Description" &&
|
||||||
|
!info.Readonly &&
|
||||||
|
!info.Builtin &&
|
||||||
|
!info.Mtime.IsZero() &&
|
||||||
|
!info.Ctime.IsZero()
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,9 @@ package mcp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
goumcp "github.com/yaoapp/gou/mcp"
|
||||||
"github.com/yaoapp/yao/dsl/types"
|
"github.com/yaoapp/yao/dsl/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,30 +22,163 @@ func NewClient(root string, fs types.IO, db types.IO) types.Manager {
|
||||||
|
|
||||||
// Loaded return all loaded DSLs
|
// Loaded return all loaded DSLs
|
||||||
func (client *YaoMCPClient) Loaded(ctx context.Context) (map[string]*types.Info, error) {
|
func (client *YaoMCPClient) Loaded(ctx context.Context) (map[string]*types.Info, error) {
|
||||||
return nil, nil
|
infos := map[string]*types.Info{}
|
||||||
|
|
||||||
|
// Get all loaded MCP clients
|
||||||
|
clientIDs := goumcp.ListClients()
|
||||||
|
|
||||||
|
for _, id := range clientIDs {
|
||||||
|
// Get the client
|
||||||
|
mcpClient, err := goumcp.Select(id)
|
||||||
|
if err != nil {
|
||||||
|
continue // Skip if client not found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get meta info from the client
|
||||||
|
meta := mcpClient.GetMetaInfo()
|
||||||
|
|
||||||
|
infos[id] = &types.Info{
|
||||||
|
ID: id,
|
||||||
|
Path: types.ToPath(types.TypeMCPClient, id),
|
||||||
|
Type: types.TypeMCPClient,
|
||||||
|
Label: meta.Label,
|
||||||
|
Sort: meta.Sort,
|
||||||
|
Description: meta.Description,
|
||||||
|
Tags: meta.Tags,
|
||||||
|
Readonly: meta.Readonly,
|
||||||
|
Builtin: meta.Builtin,
|
||||||
|
Mtime: meta.Mtime,
|
||||||
|
Ctime: meta.Ctime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load will unload the DSL first, then load the DSL from DB or file system
|
// Load will unload the DSL first, then load the DSL from DB or file system
|
||||||
func (client *YaoMCPClient) Load(ctx context.Context, options *types.LoadOptions) error {
|
func (client *YaoMCPClient) Load(ctx context.Context, options *types.LoadOptions) error {
|
||||||
|
if options == nil {
|
||||||
|
return fmt.Errorf("load options is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ID == "" {
|
||||||
|
return fmt.Errorf("load options id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Case 1: If Source is provided, use LoadClientSource
|
||||||
|
if options.Source != "" {
|
||||||
|
_, err = goumcp.LoadClientSource(options.Source, options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if options.Path != "" && options.Store == types.StoreTypeFile {
|
||||||
|
// Case 2: If Path is provided and Store is file, use LoadClient with Path
|
||||||
|
_, err = goumcp.LoadClient(options.Path, options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if options.Store == types.StoreTypeDB {
|
||||||
|
// Case 3: If Store is db, get Source from DB first
|
||||||
|
if client.db == nil {
|
||||||
|
return fmt.Errorf("db io is required for store type db")
|
||||||
|
}
|
||||||
|
source, exists, err := client.db.Source(options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("mcp client %s not found in database", options.ID)
|
||||||
|
}
|
||||||
|
_, err = goumcp.LoadClientSource(source, options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Case 4: Default case, use LoadClient with ID
|
||||||
|
path := types.ToPath(types.TypeMCPClient, options.ID)
|
||||||
|
_, err = goumcp.LoadClient(path, options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unload will unload the DSL from memory
|
// Unload will unload the DSL from memory
|
||||||
func (client *YaoMCPClient) Unload(ctx context.Context, options *types.UnloadOptions) error {
|
func (client *YaoMCPClient) Unload(ctx context.Context, options *types.UnloadOptions) error {
|
||||||
|
if options == nil {
|
||||||
|
return fmt.Errorf("unload options is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ID == "" {
|
||||||
|
return fmt.Errorf("unload options id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the UnloadClient function from gou/mcp package
|
||||||
|
goumcp.UnloadClient(options.ID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload will unload the DSL first, then reload the DSL from DB or file system
|
// Reload will unload the DSL first, then reload the DSL from DB or file system
|
||||||
func (client *YaoMCPClient) Reload(ctx context.Context, options *types.ReloadOptions) error {
|
func (client *YaoMCPClient) Reload(ctx context.Context, options *types.ReloadOptions) error {
|
||||||
|
if options == nil {
|
||||||
|
return fmt.Errorf("reload options is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ID == "" {
|
||||||
|
return fmt.Errorf("reload options id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// First unload
|
||||||
|
goumcp.UnloadClient(options.ID)
|
||||||
|
|
||||||
|
// Then load
|
||||||
|
var err error
|
||||||
|
if options.Source != "" {
|
||||||
|
_, err = goumcp.LoadClientSource(options.Source, options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if options.Path != "" && options.Store == types.StoreTypeFile {
|
||||||
|
_, err = goumcp.LoadClient(options.Path, options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if options.Store == types.StoreTypeDB {
|
||||||
|
if client.db == nil {
|
||||||
|
return fmt.Errorf("db io is required for store type db")
|
||||||
|
}
|
||||||
|
source, exists, err := client.db.Source(options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("mcp client %s not found in database", options.ID)
|
||||||
|
}
|
||||||
|
_, err = goumcp.LoadClientSource(source, options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path := types.ToPath(types.TypeMCPClient, options.ID)
|
||||||
|
_, err = goumcp.LoadClient(path, options.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate will validate the DSL from source
|
// Validate will validate the DSL from source
|
||||||
func (client *YaoMCPClient) Validate(ctx context.Context, source string) (bool, []types.LintMessage) {
|
func (client *YaoMCPClient) Validate(ctx context.Context, source string) (bool, []types.LintMessage) {
|
||||||
return false, nil
|
return true, []types.LintMessage{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute will execute the DSL
|
// Execute will execute the DSL
|
||||||
func (client *YaoMCPClient) Execute(ctx context.Context, id string, method string, args ...any) (any, error) {
|
func (client *YaoMCPClient) Execute(ctx context.Context, id string, method string, args ...any) (any, error) {
|
||||||
return nil, nil
|
return nil, fmt.Errorf("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
330
dsl/mcp/client_test.go
Normal file
330
dsl/mcp/client_test.go
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/yao/dsl/io"
|
||||||
|
"github.com/yaoapp/yao/dsl/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMCPClientLoad(t *testing.T) {
|
||||||
|
testCase := NewTestCase()
|
||||||
|
fsio := io.NewFS(types.TypeMCPClient)
|
||||||
|
dbio := io.NewDB(types.TypeMCPClient)
|
||||||
|
manager := NewClient("mcps", fsio, dbio)
|
||||||
|
|
||||||
|
// Test Load with nil options
|
||||||
|
err := manager.Load(context.Background(), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "load options is required")
|
||||||
|
|
||||||
|
// Test Load with empty ID
|
||||||
|
err = manager.Load(context.Background(), &types.LoadOptions{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "load options id is required")
|
||||||
|
|
||||||
|
// Test Load with Source
|
||||||
|
err = manager.Load(context.Background(), testCase.LoadOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Load from filesystem
|
||||||
|
err = fsio.Create(testCase.CreateOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
path := types.ToPath(types.TypeMCPClient, testCase.ID)
|
||||||
|
err = manager.Load(context.Background(), &types.LoadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Path: path,
|
||||||
|
Store: types.StoreTypeFile,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Load from database
|
||||||
|
err = dbio.Create(testCase.CreateOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager.Load(context.Background(), &types.LoadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Store: types.StoreTypeDB,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
err = fsio.Delete(testCase.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = dbio.Delete(testCase.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientUnload(t *testing.T) {
|
||||||
|
testCase := NewTestCase()
|
||||||
|
fsio := io.NewFS(types.TypeMCPClient)
|
||||||
|
dbio := io.NewDB(types.TypeMCPClient)
|
||||||
|
manager := NewClient("mcps", fsio, dbio)
|
||||||
|
|
||||||
|
// Test Unload with nil options
|
||||||
|
err := manager.Unload(context.Background(), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unload options is required")
|
||||||
|
|
||||||
|
// Test Unload with empty ID
|
||||||
|
err = manager.Unload(context.Background(), &types.UnloadOptions{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unload options id is required")
|
||||||
|
|
||||||
|
// Load and then unload from filesystem
|
||||||
|
err = fsio.Create(testCase.CreateOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager.Load(context.Background(), &types.LoadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Store: types.StoreTypeFile,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager.Unload(context.Background(), testCase.UnloadOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
err = fsio.Delete(testCase.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientReload(t *testing.T) {
|
||||||
|
testCase := NewTestCase()
|
||||||
|
fsio := io.NewFS(types.TypeMCPClient)
|
||||||
|
dbio := io.NewDB(types.TypeMCPClient)
|
||||||
|
manager := NewClient("mcps", fsio, dbio)
|
||||||
|
|
||||||
|
// Test Reload with nil options
|
||||||
|
err := manager.Reload(context.Background(), nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "reload options is required")
|
||||||
|
|
||||||
|
// Test Reload with empty ID
|
||||||
|
err = manager.Reload(context.Background(), &types.ReloadOptions{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "reload options id is required")
|
||||||
|
|
||||||
|
// Load and then reload from filesystem
|
||||||
|
err = fsio.Create(testCase.CreateOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager.Load(context.Background(), &types.LoadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Store: types.StoreTypeFile,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager.Reload(context.Background(), testCase.ReloadOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
err = fsio.Delete(testCase.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientLoaded(t *testing.T) {
|
||||||
|
testCase := NewTestCase()
|
||||||
|
fsio := io.NewFS(types.TypeMCPClient)
|
||||||
|
dbio := io.NewDB(types.TypeMCPClient)
|
||||||
|
manager := NewClient("mcps", fsio, dbio)
|
||||||
|
|
||||||
|
// Load from filesystem
|
||||||
|
err := fsio.Create(testCase.CreateOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager.Load(context.Background(), &types.LoadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Store: types.StoreTypeFile,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Loaded
|
||||||
|
infos, err := manager.Loaded(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, infos)
|
||||||
|
assert.Contains(t, infos, testCase.ID)
|
||||||
|
|
||||||
|
// Verify metadata fields
|
||||||
|
fsInfo := infos[testCase.ID]
|
||||||
|
assert.Equal(t, testCase.ID, fsInfo.ID)
|
||||||
|
assert.Equal(t, types.TypeMCPClient, fsInfo.Type)
|
||||||
|
assert.Equal(t, testCase.Label, fsInfo.Label)
|
||||||
|
assert.Equal(t, testCase.Description, fsInfo.Description)
|
||||||
|
assert.ElementsMatch(t, testCase.Tags, fsInfo.Tags)
|
||||||
|
assert.False(t, fsInfo.Readonly)
|
||||||
|
assert.False(t, fsInfo.Builtin)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
err = fsio.Delete(testCase.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientValidate(t *testing.T) {
|
||||||
|
manager := NewClient("mcps", nil, nil)
|
||||||
|
|
||||||
|
// Test Validate
|
||||||
|
valid, messages := manager.Validate(context.Background(), "test source")
|
||||||
|
assert.True(t, valid)
|
||||||
|
assert.Empty(t, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientExecute(t *testing.T) {
|
||||||
|
manager := NewClient("mcps", nil, nil)
|
||||||
|
|
||||||
|
// Test Execute
|
||||||
|
result, err := manager.Execute(context.Background(), "test_id", "test_method")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Not implemented")
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientHTTPLoad(t *testing.T) {
|
||||||
|
testCase := NewHTTPTestCase()
|
||||||
|
fsio := io.NewFS(types.TypeMCPClient)
|
||||||
|
dbio := io.NewDB(types.TypeMCPClient)
|
||||||
|
manager := NewClient("mcps", fsio, dbio)
|
||||||
|
|
||||||
|
// Test Load with HTTP Source
|
||||||
|
err := manager.Load(context.Background(), testCase.LoadOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Loaded
|
||||||
|
infos, err := manager.Loaded(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, infos)
|
||||||
|
assert.Contains(t, infos, testCase.ID)
|
||||||
|
|
||||||
|
// Verify HTTP metadata fields
|
||||||
|
httpInfo := infos[testCase.ID]
|
||||||
|
assert.Equal(t, testCase.ID, httpInfo.ID)
|
||||||
|
assert.Equal(t, types.TypeMCPClient, httpInfo.Type)
|
||||||
|
assert.Equal(t, "Test HTTP MCP Client", httpInfo.Label)
|
||||||
|
assert.Equal(t, "Test HTTP MCP Client Description", httpInfo.Description)
|
||||||
|
assert.Contains(t, httpInfo.Tags, "http")
|
||||||
|
assert.False(t, httpInfo.Readonly)
|
||||||
|
assert.False(t, httpInfo.Builtin)
|
||||||
|
|
||||||
|
// Test Unload
|
||||||
|
err = manager.Unload(context.Background(), testCase.UnloadOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientSSELoad(t *testing.T) {
|
||||||
|
testCase := NewSSETestCase()
|
||||||
|
fsio := io.NewFS(types.TypeMCPClient)
|
||||||
|
dbio := io.NewDB(types.TypeMCPClient)
|
||||||
|
manager := NewClient("mcps", fsio, dbio)
|
||||||
|
|
||||||
|
// Test Load with SSE Source
|
||||||
|
err := manager.Load(context.Background(), testCase.LoadOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Loaded
|
||||||
|
infos, err := manager.Loaded(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, infos)
|
||||||
|
assert.Contains(t, infos, testCase.ID)
|
||||||
|
|
||||||
|
// Verify SSE metadata fields
|
||||||
|
sseInfo := infos[testCase.ID]
|
||||||
|
assert.Equal(t, testCase.ID, sseInfo.ID)
|
||||||
|
assert.Equal(t, types.TypeMCPClient, sseInfo.Type)
|
||||||
|
assert.Equal(t, "Test SSE MCP Client", sseInfo.Label)
|
||||||
|
assert.Equal(t, "Test SSE MCP Client Description", sseInfo.Description)
|
||||||
|
assert.Contains(t, sseInfo.Tags, "sse")
|
||||||
|
assert.False(t, sseInfo.Readonly)
|
||||||
|
assert.False(t, sseInfo.Builtin)
|
||||||
|
|
||||||
|
// Test Unload
|
||||||
|
err = manager.Unload(context.Background(), testCase.UnloadOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientLoadWithDatabaseStore(t *testing.T) {
|
||||||
|
testCase := NewTestCase()
|
||||||
|
fsio := io.NewFS(types.TypeMCPClient)
|
||||||
|
dbio := io.NewDB(types.TypeMCPClient)
|
||||||
|
manager := NewClient("mcps", fsio, dbio)
|
||||||
|
|
||||||
|
// Create in database first
|
||||||
|
err := dbio.Create(testCase.CreateOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Load from database
|
||||||
|
err = manager.Load(context.Background(), &types.LoadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Store: types.StoreTypeDB,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Loaded
|
||||||
|
infos, err := manager.Loaded(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, infos)
|
||||||
|
assert.Contains(t, infos, testCase.ID)
|
||||||
|
|
||||||
|
// Verify metadata fields
|
||||||
|
dbInfo := infos[testCase.ID]
|
||||||
|
assert.Equal(t, testCase.ID, dbInfo.ID)
|
||||||
|
assert.Equal(t, types.TypeMCPClient, dbInfo.Type)
|
||||||
|
assert.Equal(t, testCase.Label, dbInfo.Label)
|
||||||
|
assert.Equal(t, testCase.Description, dbInfo.Description)
|
||||||
|
assert.ElementsMatch(t, testCase.Tags, dbInfo.Tags)
|
||||||
|
assert.False(t, dbInfo.Readonly)
|
||||||
|
assert.False(t, dbInfo.Builtin)
|
||||||
|
|
||||||
|
// Test Reload from database
|
||||||
|
err = manager.Reload(context.Background(), &types.ReloadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Source: testCase.UpdatedSource,
|
||||||
|
Store: types.StoreTypeDB,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
err = dbio.Delete(testCase.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMCPClientLoadWithFileStore(t *testing.T) {
|
||||||
|
testCase := NewTestCase()
|
||||||
|
fsio := io.NewFS(types.TypeMCPClient)
|
||||||
|
dbio := io.NewDB(types.TypeMCPClient)
|
||||||
|
manager := NewClient("mcps", fsio, dbio)
|
||||||
|
|
||||||
|
// Create in file system first
|
||||||
|
err := fsio.Create(testCase.CreateOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Load from file system with explicit path
|
||||||
|
path := types.ToPath(types.TypeMCPClient, testCase.ID)
|
||||||
|
err = manager.Load(context.Background(), &types.LoadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Path: path,
|
||||||
|
Store: types.StoreTypeFile,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Loaded
|
||||||
|
infos, err := manager.Loaded(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, infos)
|
||||||
|
assert.Contains(t, infos, testCase.ID)
|
||||||
|
|
||||||
|
// Test Reload from file system
|
||||||
|
err = manager.Reload(context.Background(), &types.ReloadOptions{
|
||||||
|
ID: testCase.ID,
|
||||||
|
Path: path,
|
||||||
|
Source: testCase.UpdatedSource,
|
||||||
|
Store: types.StoreTypeFile,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
err = fsio.Delete(testCase.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
|
|
@ -96,6 +96,7 @@ require (
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
|
github.com/mark3labs/mcp-go v0.32.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
||||||
|
|
@ -131,6 +132,7 @@ require (
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/xuri/efp v0.0.1 // indirect
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
github.com/xuri/nfp v0.0.1 // indirect
|
github.com/xuri/nfp v0.0.1 // indirect
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -181,6 +181,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
|
||||||
|
github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
|
@ -300,6 +302,8 @@ github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Q
|
||||||
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||||
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
||||||
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue