diff --git a/.gitignore b/.gitignore index b72a2d6..28caba2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /services/mo* - +/bin/moling \ No newline at end of file diff --git a/cli/cmd/config.go b/cli/cmd/config.go index ebb8d2d..9deac9f 100644 --- a/cli/cmd/config.go +++ b/cli/cmd/config.go @@ -21,12 +21,14 @@ import ( "context" "encoding/json" "fmt" - "github.com/gojue/moling/services" - "github.com/rs/zerolog" - "github.com/spf13/cobra" "os" "path/filepath" "time" + + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/services" + "github.com/rs/zerolog" + "github.com/spf13/cobra" ) var configCmd = &cobra.Command{ @@ -50,8 +52,8 @@ func ConfigCommandFunc(command *cobra.Command, args []string) error { logger = zerolog.New(multi).With().Timestamp().Logger() mlConfig.SetLogger(logger) logger.Info().Msg("Start to show config") - ctx := context.WithValue(context.Background(), services.MoLingConfigKey, mlConfig) - ctx = context.WithValue(ctx, services.MoLingLoggerKey, logger) + ctx := context.WithValue(context.Background(), comm.MoLingConfigKey, mlConfig) + ctx = context.WithValue(ctx, comm.MoLingLoggerKey, logger) // 当前配置文件检测 hasConfig := false diff --git a/cli/cmd/perrun.go b/cli/cmd/perrun.go index c3f8d8f..257d167 100644 --- a/cli/cmd/perrun.go +++ b/cli/cmd/perrun.go @@ -17,9 +17,10 @@ package cmd import ( - "github.com/gojue/moling/utils" - "github.com/spf13/cobra" "path/filepath" + + "github.com/gojue/moling/pkg/utils" + "github.com/spf13/cobra" ) // mlsCommandPreFunc is a pre-run function for the MoLing command. diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2428ff4..8e281c5 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -20,11 +20,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/gojue/moling/cli/cobrautl" - "github.com/gojue/moling/services" - "github.com/gojue/moling/utils" - "github.com/rs/zerolog" - "github.com/spf13/cobra" "os" "os/signal" "os/user" @@ -33,6 +28,16 @@ import ( "sync" "syscall" "time" + + "github.com/gojue/moling/cli/cobrautl" + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/config" + "github.com/gojue/moling/pkg/server" + "github.com/gojue/moling/pkg/services" + "github.com/gojue/moling/pkg/services/abstract" + "github.com/gojue/moling/pkg/utils" + "github.com/rs/zerolog" + "github.com/spf13/cobra" ) const ( @@ -78,7 +83,7 @@ const ( var ( GitVersion = "unknown_arm64_v0.0.0_2025-03-22 20:08" - mlConfig = &services.MoLingConfig{ + mlConfig = &config.MoLingConfig{ Version: GitVersion, ConfigFile: filepath.Join("config", MLConfigName), BasePath: filepath.Join(os.TempDir(), MLRootPath), // will set in mlsCommandPreFunc @@ -188,15 +193,15 @@ func mlsCommandFunc(command *cobra.Command, args []string) error { } } loger.Info().Str("config_file", configFilePath).Msg("load config file") - ctx := context.WithValue(context.Background(), services.MoLingConfigKey, mlConfig) - ctx = context.WithValue(ctx, services.MoLingLoggerKey, loger) + ctx := context.WithValue(context.Background(), comm.MoLingConfigKey, mlConfig) + ctx = context.WithValue(ctx, comm.MoLingLoggerKey, loger) ctxNew, cancelFunc := context.WithCancel(ctx) var modules []string if mlConfig.Module != "all" { modules = strings.Split(mlConfig.Module, ",") } - var srvs []services.Service + var srvs []abstract.Service var closers = make(map[string]func() error) for srvName, nsv := range services.ServiceList() { if len(modules) > 0 { @@ -228,7 +233,7 @@ func mlsCommandFunc(command *cobra.Command, args []string) error { closers[string(srv.Name())] = srv.Close } // MCPServer - srv, err := services.NewMoLingServer(ctxNew, srvs, *mlConfig) + srv, err := server.NewMoLingServer(ctxNew, srvs, *mlConfig) if err != nil { loger.Error().Err(err).Msg("failed to create server") cancelFunc() diff --git a/pkg/comm/comm.go b/pkg/comm/comm.go new file mode 100644 index 0000000..2da6b4f --- /dev/null +++ b/pkg/comm/comm.go @@ -0,0 +1,55 @@ +// Copyright 2025 CFC4N . All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Repository: https://github.com/gojue/moling + +package comm + +import ( + "context" + "os" + "path/filepath" + + "github.com/gojue/moling/pkg/config" + "github.com/rs/zerolog" +) + +type MoLingServerType string + +type contextKey string + +// MoLingConfigKey is a context key for storing the version of MoLing +const ( + MoLingConfigKey contextKey = "moling_config" + MoLingLoggerKey contextKey = "moling_logger" +) + +// InitTestEnv initializes the test environment by creating a temporary log file and setting up the logger. +func InitTestEnv() (zerolog.Logger, context.Context, error) { + logFile := filepath.Join(os.TempDir(), "moling.log") + zerolog.SetGlobalLevel(zerolog.DebugLevel) + var logger zerolog.Logger + f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) + if err != nil { + return zerolog.Logger{}, nil, err + } + logger = zerolog.New(f).With().Timestamp().Logger() + mlConfig := &config.MoLingConfig{ + ConfigFile: filepath.Join("config", "test_config.json"), + BasePath: os.TempDir(), + } + ctx := context.WithValue(context.Background(), MoLingConfigKey, mlConfig) + ctx = context.WithValue(ctx, MoLingLoggerKey, logger) + return logger, ctx, nil +} diff --git a/services/register.go b/pkg/comm/errors.go similarity index 60% rename from services/register.go rename to pkg/comm/errors.go index c113494..7040d43 100644 --- a/services/register.go +++ b/pkg/comm/errors.go @@ -14,21 +14,10 @@ // // Repository: https://github.com/gojue/moling -package services +package comm -import ( - "context" -) - -var serviceLists = make(map[MoLingServerType]func(ctx context.Context) (Service, error)) +import "errors" -// RegisterServ register service -func RegisterServ(n MoLingServerType, f func(ctx context.Context) (Service, error)) { - //serviceLists = append(, f) - serviceLists[n] = f -} - -// ServiceList get service lists -func ServiceList() map[MoLingServerType]func(ctx context.Context) (Service, error) { - return serviceLists -} +var ( + ErrConfigNotLoaded = errors.New("config not loaded, please call LoadConfig() first") +) diff --git a/services/config.go b/pkg/config/config.go similarity index 72% rename from services/config.go rename to pkg/config/config.go index 4869cf9..75d5aa0 100644 --- a/services/config.go +++ b/pkg/config/config.go @@ -14,12 +14,10 @@ // // Repository: https://github.com/gojue/moling -package services +package config import ( - "fmt" "github.com/rs/zerolog" - "reflect" ) // Config is an interface that defines a method for checking configuration validity. @@ -61,35 +59,3 @@ func (cfg *MoLingConfig) Logger() zerolog.Logger { func (cfg *MoLingConfig) SetLogger(logger zerolog.Logger) { cfg.logger = logger } - -// mergeJSONToStruct 将JSON中的字段合并到结构体中 - -func mergeJSONToStruct(target interface{}, jsonMap map[string]interface{}) error { - // 获取目标结构体的反射值 - val := reflect.ValueOf(target).Elem() - typ := val.Type() - - // 遍历JSON map中的每个字段 - for jsonKey, jsonValue := range jsonMap { - // 遍历结构体的每个字段 - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - // 检查JSON字段名是否与结构体的JSON tag匹配 - if field.Tag.Get("json") == jsonKey { - // 获取结构体字段的反射值 - fieldVal := val.Field(i) - // 检查字段是否可设置 - if fieldVal.CanSet() { - // 将JSON值转换为结构体字段的类型 - jsonVal := reflect.ValueOf(jsonValue) - if jsonVal.Type().ConvertibleTo(fieldVal.Type()) { - fieldVal.Set(jsonVal.Convert(fieldVal.Type())) - } else { - return fmt.Errorf("type mismatch for field %s, value:%v", jsonKey, jsonValue) - } - } - } - } - } - return nil -} diff --git a/services/config_test.go b/pkg/config/config_test.go similarity index 57% rename from services/config_test.go rename to pkg/config/config_test.go index d34a437..ce243bf 100644 --- a/services/config_test.go +++ b/pkg/config/config_test.go @@ -1,29 +1,27 @@ -/* - * - * Copyright 2025 CFC4N . All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Repository: https://github.com/gojue/moling - * - */ +// Copyright 2025 CFC4N . All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Repository: https://github.com/gojue/moling -package services +package config import ( "encoding/json" "os" "testing" + + "github.com/gojue/moling/pkg/utils" ) // TestConfigLoad tests the loading of the configuration from a JSON file. @@ -51,7 +49,7 @@ func TestConfigLoad(t *testing.T) { if !ok { t.Fatalf("failed to parse MoLingConfig from JSON") } - if err := mergeJSONToStruct(cfg, mlConfig); err != nil { + if err := utils.MergeJSONToStruct(cfg, mlConfig); err != nil { t.Fatalf("failed to merge JSON to struct: %v", err) } t.Logf("Config loaded, MoLing Config.BasePath: %s", cfg.BasePath) diff --git a/services/config_test.json b/pkg/config/config_test.json similarity index 100% rename from services/config_test.json rename to pkg/config/config_test.json diff --git a/services/moling.go b/pkg/server/server.go similarity index 68% rename from services/moling.go rename to pkg/server/server.go index a0fa411..683b299 100644 --- a/services/moling.go +++ b/pkg/server/server.go @@ -1,48 +1,45 @@ -/* - * - * Copyright 2025 CFC4N . All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Repository: https://github.com/gojue/moling - * - */ - -package services +// Copyright 2025 CFC4N . All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Repository: https://github.com/gojue/moling +package server import ( "context" "fmt" - "github.com/mark3labs/mcp-go/server" - "github.com/rs/zerolog" "log" "os" "strings" "time" -) -type MoLingServerType string // MoLingServerType is the type of the server + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/config" + "github.com/gojue/moling/pkg/services/abstract" + "github.com/mark3labs/mcp-go/server" + "github.com/rs/zerolog" +) type MoLingServer struct { ctx context.Context server *server.MCPServer - services []Service + services []abstract.Service logger zerolog.Logger - mlConfig MoLingConfig + mlConfig config.MoLingConfig listenAddr string // SSE mode listen address, if empty, use STDIO mode. } -func NewMoLingServer(ctx context.Context, srvs []Service, mlConfig MoLingConfig) (*MoLingServer, error) { +func NewMoLingServer(ctx context.Context, srvs []abstract.Service, mlConfig config.MoLingConfig) (*MoLingServer, error) { mcpServer := server.NewMCPServer( mlConfig.ServerName, mlConfig.Version, @@ -56,7 +53,7 @@ func NewMoLingServer(ctx context.Context, srvs []Service, mlConfig MoLingConfig) server: mcpServer, services: srvs, listenAddr: mlConfig.ListenAddr, - logger: ctx.Value(MoLingLoggerKey).(zerolog.Logger), + logger: ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger), mlConfig: mlConfig, } err := ms.init() @@ -75,7 +72,7 @@ func (m *MoLingServer) init() error { return err } -func (m *MoLingServer) loadService(srv Service) error { +func (m *MoLingServer) loadService(srv abstract.Service) error { // Add resources for r, rhf := range srv.Resources() { diff --git a/services/moling_test.go b/pkg/server/server_test.go similarity index 55% rename from services/moling_test.go rename to pkg/server/server_test.go index 2700505..0812d2b 100644 --- a/services/moling_test.go +++ b/pkg/server/server_test.go @@ -1,35 +1,36 @@ -/* - * - * Copyright 2025 CFC4N . All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Repository: https://github.com/gojue/moling - * - */ +// Copyright 2025 CFC4N . All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Repository: https://github.com/gojue/moling -package services +package server import ( - "github.com/gojue/moling/utils" "os" "path/filepath" "testing" + + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/config" + "github.com/gojue/moling/pkg/services/abstract" + "github.com/gojue/moling/pkg/services/filesystem" + "github.com/gojue/moling/pkg/utils" ) func TestNewMLServer(t *testing.T) { // Create a new MoLingConfig - mlConfig := MoLingConfig{ + mlConfig := config.MoLingConfig{ BasePath: filepath.Join(os.TempDir(), "moling_test"), } mlDirectories := []string{ @@ -49,7 +50,7 @@ func TestNewMLServer(t *testing.T) { t.Errorf("Failed to create directory %s: %v", dirName, err) } } - logger, ctx, err := initTestEnv() + logger, ctx, err := comm.InitTestEnv() if err != nil { t.Fatalf("Failed to initialize test environment: %v", err) } @@ -57,7 +58,7 @@ func TestNewMLServer(t *testing.T) { mlConfig.SetLogger(logger) // Create a new server with the filesystem service - fs, err := NewFilesystemServer(ctx) + fs, err := filesystem.NewFilesystemServer(ctx) if err != nil { t.Errorf("Failed to create filesystem server: %v", err) } @@ -65,7 +66,7 @@ func TestNewMLServer(t *testing.T) { if err != nil { t.Errorf("Failed to initialize filesystem server: %v", err) } - srvs := []Service{ + srvs := []abstract.Service{ fs, } srv, err := NewMoLingServer(ctx, srvs, mlConfig) diff --git a/pkg/services/abstract/abstract.go b/pkg/services/abstract/abstract.go new file mode 100644 index 0000000..7c0e5a6 --- /dev/null +++ b/pkg/services/abstract/abstract.go @@ -0,0 +1,59 @@ +// Copyright 2025 CFC4N . All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Repository: https://github.com/gojue/moling + +package abstract + +import ( + "context" + + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/config" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +type ServiceFactory func(ctx context.Context) (Service, error) + +// Service defines the interface for a service with various handlers and tools. +type Service interface { + Ctx() context.Context + // Resources returns a map of resources and their corresponding handler functions. + Resources() map[mcp.Resource]server.ResourceHandlerFunc + // ResourceTemplates returns a map of resource templates and their corresponding handler functions. + ResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc + // Prompts returns a map of prompts and their corresponding handler functions. + Prompts() []PromptEntry + // Tools returns a slice of server tools. + Tools() []server.ServerTool + // NotificationHandlers returns a map of notification handlers. + NotificationHandlers() map[string]server.NotificationHandlerFunc + + // Config returns the configuration of the service as a string. + Config() string + // LoadConfig loads the configuration for the service from a map. + LoadConfig(jsonData map[string]interface{}) error + + // Init initializes the service with the given context and configuration. + Init() error + + MlConfig() *config.MoLingConfig + + // Name returns the name of the service. + Name() comm.MoLingServerType + + // Close closes the service and releases any resources it holds. + Close() error +} diff --git a/services/service.go b/pkg/services/abstract/mlservice.go similarity index 70% rename from services/service.go rename to pkg/services/abstract/mlservice.go index 473b6d5..a5269a1 100644 --- a/services/service.go +++ b/pkg/services/abstract/mlservice.go @@ -14,98 +14,57 @@ // // Repository: https://github.com/gojue/moling -package services +package abstract import ( "context" - "errors" + "sync" + + "github.com/gojue/moling/pkg/config" + "github.com/gojue/moling/pkg/utils" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/rs/zerolog" - "sync" ) -type contextKey string - -// MoLingConfigKey is a context key for storing the version of MoLing -const ( - MoLingConfigKey contextKey = "moling_config" - MoLingLoggerKey contextKey = "moling_logger" -) - -var ( - ErrConfigNotLoaded = errors.New("config not loaded, please call LoadConfig() first") -) - -// Service defines the interface for a service with various handlers and tools. -type Service interface { - Ctx() context.Context - // Resources returns a map of resources and their corresponding handler functions. - Resources() map[mcp.Resource]server.ResourceHandlerFunc - // ResourceTemplates returns a map of resource templates and their corresponding handler functions. - ResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc - // Prompts returns a map of prompts and their corresponding handler functions. - Prompts() []PromptEntry - // Tools returns a slice of server tools. - Tools() []server.ServerTool - // NotificationHandlers returns a map of notification handlers. - NotificationHandlers() map[string]server.NotificationHandlerFunc - - // Config returns the configuration of the service as a string. - Config() string - // LoadConfig loads the configuration for the service from a map. - LoadConfig(jsonData map[string]interface{}) error - - // Init initializes the service with the given context and configuration. - Init() error - - MlConfig() *MoLingConfig - - // Name returns the name of the service. - Name() MoLingServerType - - // Close closes the service and releases any resources it holds. - Close() error -} - type PromptEntry struct { - prompt mcp.Prompt - phf server.PromptHandlerFunc + PromptVar mcp.Prompt + HandlerFunc server.PromptHandlerFunc } func (pe *PromptEntry) Prompt() mcp.Prompt { - return pe.prompt + return pe.PromptVar } func (pe *PromptEntry) Handler() server.PromptHandlerFunc { - return pe.phf - + return pe.HandlerFunc } // NewMLService creates a new MLService with the given context and logger. -func NewMLService(ctx context.Context, logger zerolog.Logger, cfg *MoLingConfig) MLService { +func NewMLService(ctx context.Context, logger zerolog.Logger, cfg *config.MoLingConfig) MLService { return MLService{ - ctx: ctx, - logger: logger, + Context: ctx, + Logger: logger, mlConfig: cfg, } } // MLService implements the Service interface and provides methods to manage resources, templates, prompts, tools, and notification handlers. type MLService struct { - ctx context.Context + Context context.Context + Logger zerolog.Logger // The logger for the service + lock *sync.Mutex resources map[mcp.Resource]server.ResourceHandlerFunc resourcesTemplates map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc prompts []PromptEntry tools []server.ServerTool notificationHandlers map[string]server.NotificationHandlerFunc - logger zerolog.Logger // The logger for the service - mlConfig *MoLingConfig // The configuration for the service + mlConfig *config.MoLingConfig // The configuration for the service } -// init initializes the MLService with empty maps and a mutex. -func (mls *MLService) init() error { +// InitResources initializes the MLService with empty maps and a mutex. +func (mls *MLService) InitResources() error { mls.lock = &sync.Mutex{} mls.resources = make(map[mcp.Resource]server.ResourceHandlerFunc) mls.resourcesTemplates = make(map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc) @@ -117,7 +76,7 @@ func (mls *MLService) init() error { // Ctx returns the context of the MLService. func (mls *MLService) Ctx() context.Context { - return mls.ctx + return mls.Context } // AddResource adds a resource and its handler function to the service. @@ -191,7 +150,7 @@ func (mls *MLService) NotificationHandlers() map[string]server.NotificationHandl } // MlConfig returns the configuration of the MoLing service. -func (mls *MLService) MlConfig() *MoLingConfig { +func (mls *MLService) MlConfig() *config.MoLingConfig { return mls.mlConfig } @@ -208,7 +167,7 @@ func (mls *MLService) Name() string { // LoadConfig loads the configuration for the service from a map. func (mls *MLService) LoadConfig(jsonData map[string]interface{}) error { //panic("not implemented yet") // TODO: Implement - err := mergeJSONToStruct(mls.mlConfig, jsonData) + err := utils.MergeJSONToStruct(mls.mlConfig, jsonData) if err != nil { return err } diff --git a/services/service_test.go b/pkg/services/abstract/mlservice_test.go similarity index 92% rename from services/service_test.go rename to pkg/services/abstract/mlservice_test.go index 44cacc1..4d3f380 100644 --- a/services/service_test.go +++ b/pkg/services/abstract/mlservice_test.go @@ -14,17 +14,18 @@ // // Repository: https://github.com/gojue/moling -package services +package abstract import ( "context" - "github.com/mark3labs/mcp-go/mcp" "testing" + + "github.com/mark3labs/mcp-go/mcp" ) func TestMLService_AddResource(t *testing.T) { service := &MLService{} - err := service.init() + err := service.InitResources() if err != nil { t.Fatalf("Failed to initialize MLService: %v", err) } @@ -51,7 +52,7 @@ func TestMLService_AddResource(t *testing.T) { func TestMLService_AddResourceTemplate(t *testing.T) { service := &MLService{} - err := service.init() + err := service.InitResources() if err != nil { t.Fatalf("Failed to initialize MLService: %v", err) } @@ -78,7 +79,7 @@ func TestMLService_AddResourceTemplate(t *testing.T) { func TestMLService_AddPrompt(t *testing.T) { service := &MLService{} - err := service.init() + err := service.InitResources() if err != nil { t.Fatalf("Failed to initialize MLService: %v", err) } @@ -98,8 +99,8 @@ func TestMLService_AddPrompt(t *testing.T) { }, nil } pe := PromptEntry{ - prompt: mcp.Prompt{Name: "testPrompt"}, - phf: handler, + PromptVar: mcp.Prompt{Name: "testPrompt"}, + HandlerFunc: handler, } service.AddPrompt(pe) @@ -107,15 +108,15 @@ func TestMLService_AddPrompt(t *testing.T) { t.Errorf("Expected 1 prompt, got %d", len(service.prompts)) } for _, p := range service.prompts { - if p.prompt.Name != prompt { - t.Errorf("Expected prompt name %s, got %s", prompt, p.prompt.Name) + if p.PromptVar.Name != prompt { + t.Errorf("Expected prompt name %s, got %s", prompt, p.PromptVar.Name) } } } func TestMLService_AddTool(t *testing.T) { service := &MLService{} - err := service.init() + err := service.InitResources() if err != nil { t.Fatalf("Failed to initialize MLService: %v", err) } @@ -145,7 +146,7 @@ func TestMLService_AddTool(t *testing.T) { func TestMLService_AddNotificationHandler(t *testing.T) { service := &MLService{} - err := service.init() + err := service.InitResources() if err != nil { t.Fatalf("Failed to initialize MLService: %v", err) } diff --git a/services/browser.go b/pkg/services/browser/browser.go similarity index 86% rename from services/browser.go rename to pkg/services/browser/browser.go index c1aa48d..49be41a 100644 --- a/services/browser.go +++ b/pkg/services/browser/browser.go @@ -15,31 +15,35 @@ // Repository: https://github.com/gojue/moling // Package services provides a set of services for the MoLing application. -package services +package browser import ( "context" "encoding/json" "fmt" - "github.com/chromedp/chromedp" - "github.com/gojue/moling/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/rs/zerolog" "math/rand" "os" "path/filepath" "strings" "time" + + "github.com/chromedp/chromedp" + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/config" + "github.com/gojue/moling/pkg/services/abstract" + "github.com/gojue/moling/pkg/utils" + "github.com/mark3labs/mcp-go/mcp" + "github.com/rs/zerolog" ) const ( - BrowserDataPath = "browser" // Path to store browser data - BrowserServerName MoLingServerType = "Browser" + BrowserDataPath = "browser" // Path to store browser data + BrowserServerName comm.MoLingServerType = "Browser" ) // BrowserServer represents the configuration for the browser service. type BrowserServer struct { - MLService + abstract.MLService config *BrowserConfig name string // The name of the service cancelAlloc context.CancelFunc @@ -47,24 +51,24 @@ type BrowserServer struct { } // NewBrowserServer creates a new BrowserServer instance with the given context and configuration. -func NewBrowserServer(ctx context.Context) (Service, error) { +func NewBrowserServer(ctx context.Context) (abstract.Service, error) { bc := NewBrowserConfig() - globalConf := ctx.Value(MoLingConfigKey).(*MoLingConfig) + globalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig) bc.BrowserDataPath = filepath.Join(globalConf.BasePath, BrowserDataPath) bc.DataPath = filepath.Join(globalConf.BasePath, "data") - logger, ok := ctx.Value(MoLingLoggerKey).(zerolog.Logger) + logger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger) if !ok { - return nil, fmt.Errorf("BrowserServer: invalid logger type: %T", ctx.Value(MoLingLoggerKey)) + return nil, fmt.Errorf("BrowserServer: invalid logger type: %T", ctx.Value(comm.MoLingLoggerKey)) } loggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) { e.Str("Service", string(BrowserServerName)) }) bs := &BrowserServer{ - MLService: NewMLService(ctx, logger.Hook(loggerNameHook), globalConf), + MLService: abstract.NewMLService(ctx, logger.Hook(loggerNameHook), globalConf), config: bc, } - err := bs.init() + err := bs.InitResources() if err != nil { return nil, err } @@ -101,7 +105,7 @@ func (bs *BrowserServer) Init() error { chromedp.Flag("disable-notifications", true), chromedp.Flag("disable-dev-shm-usage", true), chromedp.Flag("autoplay-policy", "user-gesture-required"), - chromedp.CombinedOutput(bs.logger), + chromedp.CombinedOutput(bs.Logger), // (1920, 1080), (1366, 768), (1440, 900), (1280, 800) chromedp.WindowSize(1280, 800), chromedp.UserDataDir(bs.config.BrowserDataPath), @@ -115,20 +119,20 @@ func (bs *BrowserServer) Init() error { opts = append(opts, chromedp.Flag("disable-webgl", true)) } - bs.ctx, bs.cancelAlloc = chromedp.NewExecAllocator(context.Background(), opts...) + bs.Context, bs.cancelAlloc = chromedp.NewExecAllocator(context.Background(), opts...) - bs.ctx, bs.cancelChrome = chromedp.NewContext(bs.ctx, - chromedp.WithErrorf(bs.logger.Error().Msgf), - chromedp.WithDebugf(bs.logger.Debug().Msgf), + bs.Context, bs.cancelChrome = chromedp.NewContext(bs.Context, + chromedp.WithErrorf(bs.Logger.Error().Msgf), + chromedp.WithDebugf(bs.Logger.Debug().Msgf), ) - pe := PromptEntry{ - prompt: mcp.Prompt{ + pe := abstract.PromptEntry{ + PromptVar: mcp.Prompt{ Name: "browser_prompt", Description: fmt.Sprintf("Get the relevant functions and prompts of the Browser MCP Server."), //Arguments: make([]mcp.PromptArgument, 0), }, - phf: bs.handlePrompt, + HandlerFunc: bs.handlePrompt, } bs.AddPrompt(pe) bs.AddTool(mcp.NewTool( @@ -272,10 +276,10 @@ func (bs *BrowserServer) initBrowser(userDataDir string) error { singletonLock := filepath.Join(userDataDir, "SingletonLock") _, err = os.Stat(singletonLock) if err == nil { - bs.logger.Debug().Msg("Browser is already running, removing SingletonLock") + bs.Logger.Debug().Msg("Browser is already running, removing SingletonLock") err = os.RemoveAll(singletonLock) if err != nil { - bs.logger.Error().Str("Lock", singletonLock).Msgf("Browser can't work due to failed removal of SingletonLock: %v", err) + bs.Logger.Error().Str("Lock", singletonLock).Msgf("Browser can't work due to failed removal of SingletonLock: %v", err) } } return nil @@ -312,7 +316,7 @@ func (bs *BrowserServer) handleNavigate(ctx context.Context, request mcp.CallToo return nil, fmt.Errorf("url must be a string") } - err := chromedp.Run(bs.ctx, chromedp.Navigate(url)) + err := chromedp.Run(bs.Context, chromedp.Navigate(url)) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to navigate: %v", err)), nil } @@ -337,12 +341,12 @@ func (bs *BrowserServer) handleScreenshot(ctx context.Context, request mcp.CallT } var buf []byte var err error - runCtx, cancelFunc := context.WithTimeout(bs.ctx, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) + runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) defer cancelFunc() if selector == "" { err = chromedp.Run(runCtx, chromedp.FullScreenshot(&buf, 90)) } else { - err = chromedp.Run(bs.ctx, chromedp.Screenshot(selector, &buf, chromedp.NodeVisible)) + err = chromedp.Run(bs.Context, chromedp.Screenshot(selector, &buf, chromedp.NodeVisible)) } if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to take screenshot: %v", err)), nil @@ -363,7 +367,7 @@ func (bs *BrowserServer) handleClick(ctx context.Context, request mcp.CallToolRe if !ok { return mcp.NewToolResultError(fmt.Sprintf("selector must be a string:%v", selector)), nil } - runCtx, cancelFunc := context.WithTimeout(bs.ctx, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) + runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) defer cancelFunc() err := chromedp.Run(runCtx, chromedp.WaitReady("body", chromedp.ByQuery), // 等待页面就绪 @@ -389,7 +393,7 @@ func (bs *BrowserServer) handleFill(ctx context.Context, request mcp.CallToolReq return mcp.NewToolResultError(fmt.Sprintf("failed to fill input field: %v, selector:%v", args["value"], selector)), nil } - runCtx, cancelFunc := context.WithTimeout(bs.ctx, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) + runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) defer cancelFunc() err := chromedp.Run(runCtx, chromedp.SendKeys(selector, value, chromedp.NodeVisible)) if err != nil { @@ -408,7 +412,7 @@ func (bs *BrowserServer) handleSelect(ctx context.Context, request mcp.CallToolR if !ok { return mcp.NewToolResultError(fmt.Sprintf("failed to select value:%v", args["value"])), nil } - runCtx, cancelFunc := context.WithTimeout(bs.ctx, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) + runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) defer cancelFunc() err := chromedp.Run(runCtx, chromedp.SetValue(selector, value, chromedp.NodeVisible)) if err != nil { @@ -425,7 +429,7 @@ func (bs *BrowserServer) handleHover(ctx context.Context, request mcp.CallToolRe return mcp.NewToolResultError(fmt.Sprintf("selector must be a string:%v", selector)), nil } var res bool - runCtx, cancelFunc := context.WithTimeout(bs.ctx, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) + runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) defer cancelFunc() err := chromedp.Run(runCtx, chromedp.Evaluate(`document.querySelector('`+selector+`').dispatchEvent(new Event('mouseover'))`, &res)) if err != nil { @@ -441,7 +445,7 @@ func (bs *BrowserServer) handleEvaluate(ctx context.Context, request mcp.CallToo return mcp.NewToolResultError("script must be a string"), nil } var result interface{} - runCtx, cancelFunc := context.WithTimeout(bs.ctx, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) + runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second) defer cancelFunc() err := chromedp.Run(runCtx, chromedp.Evaluate(script, &result)) if err != nil { @@ -451,7 +455,7 @@ func (bs *BrowserServer) handleEvaluate(ctx context.Context, request mcp.CallToo } func (bs *BrowserServer) Close() error { - bs.logger.Debug().Msg("Closing browser server") + bs.Logger.Debug().Msg("Closing browser server") bs.cancelAlloc() bs.cancelChrome() // Cancel the context to stop the browser @@ -464,25 +468,21 @@ func (bs *BrowserServer) Close() error { func (bs *BrowserServer) Config() string { cfg, err := json.Marshal(bs.config) if err != nil { - bs.logger.Err(err).Msg("failed to marshal config") + bs.Logger.Err(err).Msg("failed to marshal config") return "{}" } return string(cfg) } -func (bs *BrowserServer) Name() MoLingServerType { +func (bs *BrowserServer) Name() comm.MoLingServerType { return BrowserServerName } // LoadConfig loads the configuration from a JSON object. func (bs *BrowserServer) LoadConfig(jsonData map[string]interface{}) error { - err := mergeJSONToStruct(bs.config, jsonData) + err := utils.MergeJSONToStruct(bs.config, jsonData) if err != nil { return err } return bs.config.Check() } - -func init() { - RegisterServ(BrowserServerName, NewBrowserServer) -} diff --git a/services/browser_config.go b/pkg/services/browser/browser_config.go similarity index 99% rename from services/browser_config.go rename to pkg/services/browser/browser_config.go index a333283..1d93067 100644 --- a/services/browser_config.go +++ b/pkg/services/browser/browser_config.go @@ -15,7 +15,7 @@ // Repository: https://github.com/gojue/moling // Package services provides a set of services for the MoLing application. -package services +package browser import ( "fmt" diff --git a/services/browser_debugger.go b/pkg/services/browser/browser_debugger.go similarity index 95% rename from services/browser_debugger.go rename to pkg/services/browser/browser_debugger.go index 4525eed..48e9842 100644 --- a/services/browser_debugger.go +++ b/pkg/services/browser/browser_debugger.go @@ -15,7 +15,7 @@ // Repository: https://github.com/gojue/moling // Package services provides a set of services for the MoLing application. -package services +package browser import ( "context" @@ -36,7 +36,7 @@ func (bs *BrowserServer) handleDebugEnable(ctx context.Context, request mcp.Call } var err error - rctx, cancel := context.WithCancel(bs.ctx) + rctx, cancel := context.WithCancel(bs.Context) defer cancel() if enabled { @@ -80,7 +80,7 @@ func (bs *BrowserServer) handleSetBreakpoint(ctx context.Context, request mcp.Ca condition, _ := args["condition"].(string) var breakpointID string - rctx, cancel := context.WithCancel(bs.ctx) + rctx, cancel := context.WithCancel(bs.Context) defer cancel() err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error { t := chromedp.FromContext(ctx).Target @@ -118,7 +118,7 @@ func (bs *BrowserServer) handleRemoveBreakpoint(ctx context.Context, request mcp if !ok { return mcp.NewToolResultError("breakpointId must be a string"), nil } - rctx, cancel := context.WithCancel(bs.ctx) + rctx, cancel := context.WithCancel(bs.Context) defer cancel() err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error { t := chromedp.FromContext(ctx).Target @@ -136,7 +136,7 @@ func (bs *BrowserServer) handleRemoveBreakpoint(ctx context.Context, request mcp // handlePause handles pausing the JavaScript execution in the browser. func (bs *BrowserServer) handlePause(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - rctx, cancel := context.WithCancel(bs.ctx) + rctx, cancel := context.WithCancel(bs.Context) defer cancel() err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error { t := chromedp.FromContext(ctx).Target @@ -152,7 +152,7 @@ func (bs *BrowserServer) handlePause(ctx context.Context, request mcp.CallToolRe // handleResume handles resuming the JavaScript execution in the browser. func (bs *BrowserServer) handleResume(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - rctx, cancel := context.WithCancel(bs.ctx) + rctx, cancel := context.WithCancel(bs.Context) defer cancel() err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error { t := chromedp.FromContext(ctx).Target @@ -169,7 +169,7 @@ func (bs *BrowserServer) handleResume(ctx context.Context, request mcp.CallToolR // handleStepOver handles stepping over the next line of JavaScript code in the browser. func (bs *BrowserServer) handleGetCallstack(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var callstack interface{} - rctx, cancel := context.WithCancel(bs.ctx) + rctx, cancel := context.WithCancel(bs.Context) defer cancel() err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error { t := chromedp.FromContext(ctx).Target diff --git a/services/browser_test.go b/pkg/services/browser/browser_test.go similarity index 82% rename from services/browser_test.go rename to pkg/services/browser/browser_test.go index abdbae5..8b978b0 100644 --- a/services/browser_test.go +++ b/pkg/services/browser/browser_test.go @@ -14,34 +14,13 @@ // // Repository: https://github.com/gojue/moling -package services +package browser import ( - "context" - "github.com/rs/zerolog" - "os" - "path/filepath" "testing" -) -// initTestEnv initializes the test environment by creating a temporary log file and setting up the logger. -func initTestEnv() (zerolog.Logger, context.Context, error) { - logFile := filepath.Join(os.TempDir(), "moling.log") - zerolog.SetGlobalLevel(zerolog.DebugLevel) - var logger zerolog.Logger - f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) - if err != nil { - return zerolog.Logger{}, nil, err - } - logger = zerolog.New(f).With().Timestamp().Logger() - mlConfig := &MoLingConfig{ - ConfigFile: filepath.Join("config", "test_config.json"), - BasePath: os.TempDir(), - } - ctx := context.WithValue(context.Background(), MoLingConfigKey, mlConfig) - ctx = context.WithValue(ctx, MoLingLoggerKey, logger) - return logger, ctx, nil -} + "github.com/gojue/moling/pkg/comm" +) func TestBrowserServer(t *testing.T) { // @@ -53,7 +32,7 @@ func TestBrowserServer(t *testing.T) { // URLTimeout: 10, // SelectorQueryTimeout: 10, //} - logger, ctx, err := initTestEnv() + logger, ctx, err := comm.InitTestEnv() if err != nil { t.Fatalf("Failed to initialize test environment: %v", err) } diff --git a/services/command.go b/pkg/services/command/command.go similarity index 84% rename from services/command.go rename to pkg/services/command/command.go index 07297f3..b1796c0 100644 --- a/services/command.go +++ b/pkg/services/command/command.go @@ -15,16 +15,21 @@ // Repository: https://github.com/gojue/moling // Package services Description: This file contains the implementation of the CommandServer interface for macOS and Linux. -package services +package command import ( "context" "encoding/json" "fmt" - "github.com/mark3labs/mcp-go/mcp" - "github.com/rs/zerolog" "path/filepath" "strings" + + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/config" + "github.com/gojue/moling/pkg/services/abstract" + "github.com/gojue/moling/pkg/utils" + "github.com/mark3labs/mcp-go/mcp" + "github.com/rs/zerolog" ) var ( @@ -35,27 +40,27 @@ var ( ) const ( - CommandServerName MoLingServerType = "Command" + CommandServerName comm.MoLingServerType = "Command" ) // CommandServer implements the Service interface and provides methods to execute named commands. type CommandServer struct { - MLService + abstract.MLService config *CommandConfig osName string osVersion string } // NewCommandServer creates a new CommandServer with the given allowed commands. -func NewCommandServer(ctx context.Context) (Service, error) { +func NewCommandServer(ctx context.Context) (abstract.Service, error) { var err error cc := NewCommandConfig() - gConf, ok := ctx.Value(MoLingConfigKey).(*MoLingConfig) + gConf, ok := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig) if !ok { return nil, fmt.Errorf("CommandServer: invalid config type") } - lger, ok := ctx.Value(MoLingLoggerKey).(zerolog.Logger) + lger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger) if !ok { return nil, fmt.Errorf("CommandServer: invalid logger type") } @@ -65,11 +70,11 @@ func NewCommandServer(ctx context.Context) (Service, error) { }) cs := &CommandServer{ - MLService: NewMLService(ctx, lger.Hook(loggerNameHook), gConf), + MLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), gConf), config: cc, } - err = cs.init() + err = cs.InitResources() if err != nil { return nil, err } @@ -79,13 +84,13 @@ func NewCommandServer(ctx context.Context) (Service, error) { func (cs *CommandServer) Init() error { var err error - pe := PromptEntry{ - prompt: mcp.Prompt{ + pe := abstract.PromptEntry{ + PromptVar: mcp.Prompt{ Name: "command_prompt", Description: fmt.Sprintf("get command prompt"), //Arguments: make([]mcp.PromptArgument, 0), }, - phf: cs.handlePrompt, + HandlerFunc: cs.handlePrompt, } cs.AddPrompt(pe) cs.AddTool(mcp.NewTool( @@ -124,7 +129,7 @@ func (cs *CommandServer) handleExecuteCommand(ctx context.Context, request mcp.C // Check if the command is allowed if !cs.isAllowedCommand(command) { - cs.logger.Err(ErrCommandNotAllowed).Str("command", command).Msgf("If you want to allow this command, add it to %s", filepath.Join(cs.MlConfig().BasePath, "config", cs.MlConfig().ConfigFile)) + cs.Logger.Err(ErrCommandNotAllowed).Str("command", command).Msgf("If you want to allow this command, add it to %s", filepath.Join(cs.MlConfig().BasePath, "config", cs.MlConfig().ConfigFile)) return mcp.NewToolResultError(fmt.Sprintf("Error: Command '%s' is not allowed", command)), nil } @@ -177,26 +182,26 @@ func (cs *CommandServer) Config() string { cs.config.AllowedCommand = strings.Join(cs.config.allowedCommands, ",") cfg, err := json.Marshal(cs.config) if err != nil { - cs.logger.Err(err).Msg("failed to marshal config") + cs.Logger.Err(err).Msg("failed to marshal config") return "{}" } - cs.logger.Debug().Str("config", string(cfg)).Msg("CommandServer config") + cs.Logger.Debug().Str("config", string(cfg)).Msg("CommandServer config") return string(cfg) } -func (cs *CommandServer) Name() MoLingServerType { +func (cs *CommandServer) Name() comm.MoLingServerType { return CommandServerName } func (cs *CommandServer) Close() error { // Cancel the context to stop the browser - cs.logger.Debug().Msg("CommandServer closed") + cs.Logger.Debug().Msg("CommandServer closed") return nil } // LoadConfig loads the configuration from a JSON object. func (cs *CommandServer) LoadConfig(jsonData map[string]interface{}) error { - err := mergeJSONToStruct(cs.config, jsonData) + err := utils.MergeJSONToStruct(cs.config, jsonData) if err != nil { return err } @@ -204,7 +209,3 @@ func (cs *CommandServer) LoadConfig(jsonData map[string]interface{}) error { cs.config.allowedCommands = strings.Split(cs.config.AllowedCommand, ",") return cs.config.Check() } - -func init() { - RegisterServ(CommandServerName, NewCommandServer) -} diff --git a/services/command_config.go b/pkg/services/command/command_config.go similarity index 99% rename from services/command_config.go rename to pkg/services/command/command_config.go index a94433c..7cb7b35 100644 --- a/services/command_config.go +++ b/pkg/services/command/command_config.go @@ -14,7 +14,7 @@ // // Repository: https://github.com/gojue/moling -package services +package command import ( "fmt" diff --git a/services/command_exec.go b/pkg/services/command/command_exec.go similarity index 98% rename from services/command_exec.go rename to pkg/services/command/command_exec.go index c0bf53b..016d696 100644 --- a/services/command_exec.go +++ b/pkg/services/command/command_exec.go @@ -16,7 +16,7 @@ // // Repository: https://github.com/gojue/moling -package services +package command import ( "context" diff --git a/services/command_exec_test.go b/pkg/services/command/command_exec_test.go similarity index 97% rename from services/command_exec_test.go rename to pkg/services/command/command_exec_test.go index 1336e9f..dab9ea9 100644 --- a/services/command_exec_test.go +++ b/pkg/services/command/command_exec_test.go @@ -14,7 +14,7 @@ // // Repository: https://github.com/gojue/moling -package services +package command import ( "context" @@ -23,6 +23,8 @@ import ( "reflect" "testing" "time" + + "github.com/gojue/moling/pkg/comm" ) // MockCommandServer is a mock implementation of CommandServer for testing purposes. @@ -65,7 +67,7 @@ func TestExecuteCommand(t *testing.T) { func TestAllowCmd(t *testing.T) { // Test with a command that is allowed - _, ctx, err := initTestEnv() + _, ctx, err := comm.InitTestEnv() if err != nil { t.Fatalf("Failed to initialize test environment: %v", err) } diff --git a/services/command_exec_windows.go b/pkg/services/command/command_exec_windows.go similarity index 98% rename from services/command_exec_windows.go rename to pkg/services/command/command_exec_windows.go index 13c0341..b2a0229 100644 --- a/services/command_exec_windows.go +++ b/pkg/services/command/command_exec_windows.go @@ -17,7 +17,7 @@ // Repository: https://github.com/gojue/moling // Package services Description: This file contains the implementation of the CommandServer interface for Windows. -package services +package command import ( "os/exec" diff --git a/services/file_system.go b/pkg/services/filesystem/file_system.go similarity index 93% rename from services/file_system.go rename to pkg/services/filesystem/file_system.go index f6acf00..bac1a6d 100644 --- a/services/file_system.go +++ b/pkg/services/filesystem/file_system.go @@ -16,19 +16,24 @@ // Source: https://github.com/mark3labs/mcp-filesystem-server // Package services provides the implementation of the FileSystemServer, which allows access to files and directories on the local file system. -package services +package filesystem import ( "context" "encoding/base64" "encoding/json" "fmt" - "github.com/mark3labs/mcp-go/mcp" - "github.com/rs/zerolog" "os" "path/filepath" "strings" "time" + + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/config" + "github.com/gojue/moling/pkg/services/abstract" + "github.com/gojue/moling/pkg/utils" + "github.com/mark3labs/mcp-go/mcp" + "github.com/rs/zerolog" ) const ( @@ -38,7 +43,7 @@ const ( MaxBase64Size = 1024 * 1024 * 1 ) const ( - FilesystemServerName MoLingServerType = "FileSystem" + FilesystemServerName comm.MoLingServerType = "FileSystem" ) type FileInfo struct { @@ -52,19 +57,19 @@ type FileInfo struct { } type FilesystemServer struct { - MLService + abstract.MLService config *FileSystemConfig } -func NewFilesystemServer(ctx context.Context) (Service, error) { +func NewFilesystemServer(ctx context.Context) (abstract.Service, error) { // Validate the config var err error - globalConf := ctx.Value(MoLingConfigKey).(*MoLingConfig) + globalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig) userDataDir := filepath.Join(globalConf.BasePath, "data") fc := NewFileSystemConfig(userDataDir) - lger, ok := ctx.Value(MoLingLoggerKey).(zerolog.Logger) + lger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger) if !ok { return nil, fmt.Errorf("FilesystemServer: invalid logger type") } @@ -74,11 +79,11 @@ func NewFilesystemServer(ctx context.Context) (Service, error) { }) fs := &FilesystemServer{ - MLService: NewMLService(ctx, lger.Hook(loggerNameHook), globalConf), + MLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), globalConf), config: fc, } - err = fs.init() + err = fs.InitResources() if err != nil { return nil, fmt.Errorf("failed to initialize filesystem server: %v", err) } @@ -92,12 +97,12 @@ func (fs *FilesystemServer) Init() error { mcp.WithResourceDescription("Access to files and directories on the local file system"), ), fs.handleReadResource) - pe := PromptEntry{ - prompt: mcp.Prompt{ + pe := abstract.PromptEntry{ + PromptVar: mcp.Prompt{ Name: "filesystem_prompt", Description: fmt.Sprintf("Get the relevant functions and prompts of the FileSystem MCP Server."), }, - phf: fs.handlePrompt, + HandlerFunc: fs.handlePrompt, } fs.AddPrompt(pe) @@ -332,7 +337,7 @@ func (fs *FilesystemServer) searchFiles(rootPath, pattern string) ([]string, err // Resource handler func (fs *FilesystemServer) handleReadResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { uri := request.Params.URI - fs.logger.Debug().Str("uri", uri).Msg("handleReadResource") + fs.Logger.Debug().Str("uri", uri).Msg("handleReadResource") // Check if it'fss a file:// URI if !strings.HasPrefix(uri, "file://") { @@ -366,7 +371,7 @@ func (fs *FilesystemServer) handleReadResource(ctx context.Context, request mcp. for _, entry := range entries { entryPath := filepath.Join(validPath, entry.Name()) - entryURI := pathToResourceURI(entryPath) + entryURI := utils.PathToResourceURI(entryPath) if entry.IsDir() { result.WriteString(fmt.Sprintf("[DIR] %s (%s)\n", entry.Name(), entryURI)) @@ -391,7 +396,7 @@ func (fs *FilesystemServer) handleReadResource(ctx context.Context, request mcp. } // It'fss a file, determine how to handle it - mimeType := detectMimeType(validPath) + mimeType := utils.DetectMimeType(validPath) // Check file size if fileInfo.Size() > MaxInlineSize { @@ -412,7 +417,7 @@ func (fs *FilesystemServer) handleReadResource(ctx context.Context, request mcp. } // Handle based on content type - if isTextFile(mimeType) { + if utils.IsTextFile(mimeType) { // It'fss a text file, return as text return []mcp.ResourceContents{ mcp.TextResourceContents{ @@ -469,7 +474,7 @@ func (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.Call if info.IsDir() { // For directories, return a resource reference instead - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -489,12 +494,12 @@ func (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.Call } // Determine MIME type - mimeType := detectMimeType(validPath) + mimeType := utils.DetectMimeType(validPath) // Check file size if info.Size() > MaxInlineSize { // File is too large to inline, return a resource reference - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -520,10 +525,10 @@ func (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.Call } // Handle based on content type - if isTextFile(mimeType) { + if utils.IsTextFile(mimeType) { // It'fss a text file, return as text return mcp.NewToolResultText(string(content)), nil - } else if isImageFile(mimeType) { + } else if utils.IsImageFile(mimeType) { // It'fss an image file, return as image content if info.Size() <= MaxBase64Size { return &mcp.CallToolResult{ @@ -541,7 +546,7 @@ func (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.Call }, nil } else { // Too large for base64, return a reference - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -561,7 +566,7 @@ func (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.Call } } else { // It'fss another type of binary file - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) if info.Size() <= MaxBase64Size { // Small enough for base64 encoding @@ -651,7 +656,7 @@ func (fs *FilesystemServer) handleWriteFile(ctx context.Context, request mcp.Cal return mcp.NewToolResultText(fmt.Sprintf("Successfully wrote to %s", path)), nil } - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -702,7 +707,7 @@ func (fs *FilesystemServer) handleListDirectory(ctx context.Context, request mcp for _, entry := range entries { entryPath := filepath.Join(validPath, entry.Name()) - resourceURI := pathToResourceURI(entryPath) + resourceURI := utils.PathToResourceURI(entryPath) if entry.IsDir() { result.WriteString(fmt.Sprintf("[DIR] %s (%s)\n", entry.Name(), resourceURI)) @@ -718,7 +723,7 @@ func (fs *FilesystemServer) handleListDirectory(ctx context.Context, request mcp } // Return both text content and embedded resource - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -752,7 +757,7 @@ func (fs *FilesystemServer) handleCreateDirectory(ctx context.Context, request m // Check if path already exists if info, err := os.Stat(validPath); err == nil { if info.IsDir() { - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -777,7 +782,7 @@ func (fs *FilesystemServer) handleCreateDirectory(ctx context.Context, request m return mcp.NewToolResultError(fmt.Sprintf("Error creating directory: %v", err)), nil } - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -832,7 +837,7 @@ func (fs *FilesystemServer) handleMoveFile(ctx context.Context, request mcp.Call return mcp.NewToolResultError(fmt.Sprintf("Error moving file: %v", err)), nil } - resourceURI := pathToResourceURI(validDest) + resourceURI := utils.PathToResourceURI(validDest) return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -895,7 +900,7 @@ func (fs *FilesystemServer) handleSearchFiles(ctx context.Context, request mcp.C formattedResults.WriteString(fmt.Sprintf("Found %d results:\n\n", len(results))) for _, result := range results { - resourceURI := pathToResourceURI(result) + resourceURI := utils.PathToResourceURI(result) info, err := os.Stat(result) if err == nil { if info.IsDir() { @@ -940,10 +945,10 @@ func (fs *FilesystemServer) handleGetFileInfo(ctx context.Context, request mcp.C // Get MIME type for files mimeType := "directory" if info.IsFile { - mimeType = detectMimeType(validPath) + mimeType = utils.DetectMimeType(validPath) } - resourceURI := pathToResourceURI(validPath) + resourceURI := utils.PathToResourceURI(validPath) // Determine file type text var fileTypeText string @@ -998,7 +1003,7 @@ func (fs *FilesystemServer) handleListAllowedDirectories(ctx context.Context, re result.WriteString("Allowed directories:") for _, dir := range displayDirs { - resourceURI := pathToResourceURI(dir) + resourceURI := utils.PathToResourceURI(dir) result.WriteString(fmt.Sprintf("%s (%s)\n", dir, resourceURI)) } @@ -1010,32 +1015,28 @@ func (fs *FilesystemServer) Config() string { fs.config.AllowedDir = strings.Join(fs.config.allowedDirs, ",") cfg, err := json.Marshal(fs.config) if err != nil { - fs.logger.Err(err).Msg("failed to marshal config") + fs.Logger.Err(err).Msg("failed to marshal config") return "{}" } return string(cfg) } -func (fs *FilesystemServer) Name() MoLingServerType { +func (fs *FilesystemServer) Name() comm.MoLingServerType { return FilesystemServerName } func (fs *FilesystemServer) Close() error { // Cancel the context to stop the browser - fs.logger.Debug().Msg("closing FilesystemServer") + fs.Logger.Debug().Msg("closing FilesystemServer") return nil } // LoadConfig loads the configuration from a JSON object. func (fs *FilesystemServer) LoadConfig(jsonData map[string]interface{}) error { - err := mergeJSONToStruct(fs.config, jsonData) + err := utils.MergeJSONToStruct(fs.config, jsonData) if err != nil { return err } fs.config.allowedDirs = strings.Split(fs.config.AllowedDir, ",") return fs.config.Check() } - -func init() { - RegisterServ(FilesystemServerName, NewFilesystemServer) -} diff --git a/services/file_system_config.go b/pkg/services/filesystem/file_system_config.go similarity index 99% rename from services/file_system_config.go rename to pkg/services/filesystem/file_system_config.go index 2788e3f..998eab7 100644 --- a/services/file_system_config.go +++ b/pkg/services/filesystem/file_system_config.go @@ -14,7 +14,7 @@ // // Repository: https://github.com/gojue/moling -package services +package filesystem import ( "fmt" diff --git a/services/file_system_windows.go b/pkg/services/filesystem/file_system_windows.go similarity index 97% rename from services/file_system_windows.go rename to pkg/services/filesystem/file_system_windows.go index 44bed44..bd917a5 100644 --- a/services/file_system_windows.go +++ b/pkg/services/filesystem/file_system_windows.go @@ -16,7 +16,7 @@ * Repository: https://github.com/gojue/moling */ -package services +package filesystem import "os" diff --git a/pkg/services/register.go b/pkg/services/register.go new file mode 100644 index 0000000..039a230 --- /dev/null +++ b/pkg/services/register.go @@ -0,0 +1,46 @@ +// Copyright 2025 CFC4N . All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Repository: https://github.com/gojue/moling + +package services + +import ( + "github.com/gojue/moling/pkg/comm" + "github.com/gojue/moling/pkg/services/abstract" + "github.com/gojue/moling/pkg/services/browser" + "github.com/gojue/moling/pkg/services/command" + "github.com/gojue/moling/pkg/services/filesystem" +) + +var serviceLists = make(map[comm.MoLingServerType]abstract.ServiceFactory) + +// RegisterServ register service +func RegisterServ(n comm.MoLingServerType, f abstract.ServiceFactory) { + serviceLists[n] = f +} + +// ServiceList get service lists +func ServiceList() map[comm.MoLingServerType]abstract.ServiceFactory { + return serviceLists +} + +func init() { + // Register the filesystem service + RegisterServ(filesystem.FilesystemServerName, filesystem.NewFilesystemServer) + // Register the browser service + RegisterServ(browser.BrowserServerName, browser.NewBrowserServer) + // Register the command service + RegisterServ(command.CommandServerName, command.NewCommandServer) +} diff --git a/utils/pid.go b/pkg/utils/pid.go similarity index 100% rename from utils/pid.go rename to pkg/utils/pid.go diff --git a/utils/pid_unix.go b/pkg/utils/pid_unix.go similarity index 100% rename from utils/pid_unix.go rename to pkg/utils/pid_unix.go diff --git a/utils/pid_windows.go b/pkg/utils/pid_windows.go similarity index 100% rename from utils/pid_windows.go rename to pkg/utils/pid_windows.go diff --git a/utils/rotewriter.go b/pkg/utils/rotewriter.go similarity index 100% rename from utils/rotewriter.go rename to pkg/utils/rotewriter.go diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..0087a62 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,134 @@ +// Copyright 2025 CFC4N . All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Repository: https://github.com/gojue/moling + +package utils + +import ( + "fmt" + "mime" + "net/http" + "os" + "path/filepath" + "reflect" + "strings" +) + +// CreateDirectory checks if a directory exists, and creates it if it doesn't +func CreateDirectory(path string) error { + _, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + err = os.MkdirAll(path, 0o755) + if err != nil { + return err + } + } else { + return err + } + } + return nil +} + +// StringInSlice checks if a string is in a slice of strings +func StringInSlice(s string, modules []string) bool { + for _, module := range modules { + if module == s { + return true + } + } + return false +} + +// MergeJSONToStruct 将JSON中的字段合并到结构体中 +func MergeJSONToStruct(target interface{}, jsonMap map[string]interface{}) error { + // 获取目标结构体的反射值 + val := reflect.ValueOf(target).Elem() + typ := val.Type() + + // 遍历JSON map中的每个字段 + for jsonKey, jsonValue := range jsonMap { + // 遍历结构体的每个字段 + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + // 检查JSON字段名是否与结构体的JSON tag匹配 + if field.Tag.Get("json") == jsonKey { + // 获取结构体字段的反射值 + fieldVal := val.Field(i) + // 检查字段是否可设置 + if fieldVal.CanSet() { + // 将JSON值转换为结构体字段的类型 + jsonVal := reflect.ValueOf(jsonValue) + if jsonVal.Type().ConvertibleTo(fieldVal.Type()) { + fieldVal.Set(jsonVal.Convert(fieldVal.Type())) + } else { + return fmt.Errorf("type mismatch for field %s, value:%v", jsonKey, jsonValue) + } + } + } + } + } + return nil +} + +// DetectMimeType tries to determine the MIME type of a file +func DetectMimeType(path string) string { + // First try by extension + ext := filepath.Ext(path) + if ext != "" { + mimeType := mime.TypeByExtension(ext) + if mimeType != "" { + return mimeType + } + } + + // If that fails, try to read a bit of the file + file, err := os.Open(path) + if err != nil { + return "application/octet-stream" // Default + } + defer file.Close() + + // Read first 512 bytes to detect content type + buffer := make([]byte, 512) + n, err := file.Read(buffer) + if err != nil { + return "application/octet-stream" // Default + } + + // Use http.DetectContentType + return http.DetectContentType(buffer[:n]) +} + +// IsTextFile determines if a file is likely a text file based on MIME type +func IsTextFile(mimeType string) bool { + return strings.HasPrefix(mimeType, "text/") || + mimeType == "application/json" || + mimeType == "application/xml" || + mimeType == "application/javascript" || + mimeType == "application/x-javascript" || + strings.Contains(mimeType, "+xml") || + strings.Contains(mimeType, "+json") +} + +// IsImageFile determines if a file is an image based on MIME type +func IsImageFile(mimeType string) bool { + return strings.HasPrefix(mimeType, "image/") +} + +// PathToResourceURI converts a file path to a resource URI +func PathToResourceURI(path string) string { + return "file://" + path +} diff --git a/services/utils.go b/services/utils.go deleted file mode 100644 index 2127fe9..0000000 --- a/services/utils.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2025 CFC4N . All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Repository: https://github.com/gojue/moling - -package services - -import ( - "mime" - "net/http" - "os" - "path/filepath" - "strings" -) - -// detectMimeType tries to determine the MIME type of a file -func detectMimeType(path string) string { - // First try by extension - ext := filepath.Ext(path) - if ext != "" { - mimeType := mime.TypeByExtension(ext) - if mimeType != "" { - return mimeType - } - } - - // If that fails, try to read a bit of the file - file, err := os.Open(path) - if err != nil { - return "application/octet-stream" // Default - } - defer file.Close() - - // Read first 512 bytes to detect content type - buffer := make([]byte, 512) - n, err := file.Read(buffer) - if err != nil { - return "application/octet-stream" // Default - } - - // Use http.DetectContentType - return http.DetectContentType(buffer[:n]) -} - -// isTextFile determines if a file is likely a text file based on MIME type -func isTextFile(mimeType string) bool { - return strings.HasPrefix(mimeType, "text/") || - mimeType == "application/json" || - mimeType == "application/xml" || - mimeType == "application/javascript" || - mimeType == "application/x-javascript" || - strings.Contains(mimeType, "+xml") || - strings.Contains(mimeType, "+json") -} - -// isImageFile determines if a file is an image based on MIME type -func isImageFile(mimeType string) bool { - return strings.HasPrefix(mimeType, "image/") -} - -// pathToResourceURI converts a file path to a resource URI -func pathToResourceURI(path string) string { - return "file://" + path -} diff --git a/utils/utils.go b/utils/utils.go deleted file mode 100644 index 7e8ecce..0000000 --- a/utils/utils.go +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2025 CFC4N . All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Repository: https://github.com/gojue/moling - */ - -package utils - -import "os" - -// CreateDirectory checks if a directory exists, and creates it if it doesn't -func CreateDirectory(path string) error { - _, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - err = os.MkdirAll(path, 0o755) - if err != nil { - return err - } - } else { - return err - } - } - return nil -} - -// StringInSlice checks if a string is in a slice of strings -func StringInSlice(s string, modules []string) bool { - for _, module := range modules { - if module == s { - return true - } - } - return false -}