#
tokens: 43299/50000 47/50 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/gojue/moling?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       ├── go-test.yml
│       └── release.yml
├── .gitignore
├── .golangci.yml
├── bin
│   └── .gitkeep
├── CHANGELOG.md
├── cli
│   ├── cmd
│   │   ├── client.go
│   │   ├── config.go
│   │   ├── perrun.go
│   │   ├── root.go
│   │   └── utils.go
│   ├── cobrautl
│   │   └── help.go
│   └── main.go
├── client
│   ├── client_config_windows.go
│   ├── client_config.go
│   ├── client_test.go
│   └── client.go
├── dist
│   └── .gitkeep
├── functions.mk
├── go.mod
├── go.sum
├── images
│   ├── logo-colorful.png
│   ├── logo.svg
│   └── screenshot_claude.png
├── install
│   ├── install.ps1
│   └── install.sh
├── LICENSE
├── main.go
├── Makefile
├── Makefile.release
├── pkg
│   ├── comm
│   │   ├── comm.go
│   │   └── errors.go
│   ├── config
│   │   ├── config_test.go
│   │   ├── config_test.json
│   │   └── config.go
│   ├── server
│   │   ├── server_test.go
│   │   └── server.go
│   ├── services
│   │   ├── abstract
│   │   │   ├── abstract.go
│   │   │   ├── mlservice_test.go
│   │   │   └── mlservice.go
│   │   ├── browser
│   │   │   ├── browser_config.go
│   │   │   ├── browser_debugger.go
│   │   │   ├── browser_test.go
│   │   │   └── browser.go
│   │   ├── command
│   │   │   ├── command_config.go
│   │   │   ├── command_exec_test.go
│   │   │   ├── command_exec_windows.go
│   │   │   ├── command_exec.go
│   │   │   └── command.go
│   │   ├── filesystem
│   │   │   ├── file_system_config.go
│   │   │   ├── file_system_windows.go
│   │   │   └── file_system.go
│   │   └── register.go
│   └── utils
│       ├── pid_unix.go
│       ├── pid_windows.go
│       ├── pid.go
│       ├── rotewriter.go
│       └── utils.go
├── prompts
│   ├──  filesystem.md
│   ├── browser.md
│   └── command.md
├── README_JA_JP.md
├── README_ZH_HANS.md
├── README.md
└── variables.mk
```

# Files

--------------------------------------------------------------------------------
/bin/.gitkeep:
--------------------------------------------------------------------------------

```
1 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | /services/mo*
2 | /bin/moling
```

--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # This configuration file is not a recommendation.
 2 | #
 3 | # We intentionally use a limited set of linters.
 4 | # This configuration file is used with different version of golangci-lint to avoid regressions:
 5 | # the linters can change between version,
 6 | # their configuration may be not compatible or their reports can be different,
 7 | # and this can break some of our tests.
 8 | # Also, some linters are not relevant for the project (e.g. linters related to SQL).
 9 | #
10 | # We have specific constraints, so we use a specific configuration.
11 | #
12 | # See the file `.golangci.reference.yml` to have a list of all available configuration options.
13 | 
14 | version: "2"
15 | 
16 | linters:
17 |   default: none
18 |   # This list of linters is not a recommendation (same thing for all this configuration file).
19 |   # We intentionally use a limited set of linters.
20 |   # See the comment on top of this file.
21 |   enable:
22 |     - errcheck
23 |     - staticcheck
24 |     - errorlint
25 | 
26 |   settings:
27 |     errorlint:
28 |       asserts: false
29 |     staticcheck:
30 |       checks:
31 |         # Invalid regular expression.
32 |         # https://staticcheck.dev/docs/checks/#SA1000
33 |         - all
34 |         - "-ST1000"
35 |         - "-S1023"
36 |         - "-S1005"
37 |         - "-QF1004"
38 | 
39 |   exclusions:
40 |     paths:
41 |       - dist/
42 |       - docs/
43 |       - tests/
44 |       - bin/
45 |       - images/
46 |       - install/
47 |       - utils/
48 | 
49 | formatters:
50 |   enable:
51 |     - gofmt
52 |     - goimports
53 |   settings:
54 |     gofmt:
55 |       rewrite-rules:
56 |         - pattern: 'interface{}'
57 |           replacement: 'any'
58 |     goimports:
59 |       local-prefixes:
60 |         - github.com/gojue/moling
61 |   exclusions:
62 |     paths:
63 |       - dist/
64 |       - docs/
65 |       - tests/
66 |       - bin/
67 |       - images/
68 |       - install/
69 |       - utils/
70 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## MoLing MCP Server
  2 | 
  3 | English | [中文](./README_ZH_HANS.md) | [日本語](./README_JA_JP.md)
  4 | 
  5 | [![GitHub stars](https://img.shields.io/github/stars/gojue/moling.svg?label=Stars&logo=github)](https://github.com/gojue/moling/stargazers)
  6 | [![GitHub forks](https://img.shields.io/github/forks/gojue/moling?label=Forks&logo=github)](https://github.com/gojue/moling/forks)
  7 | [![CI](https://github.com/gojue/moling/actions/workflows/go-test.yml/badge.svg)](https://github.com/gojue/moling/actions/workflows/go-test.yml)
  8 | [![Github Version](https://img.shields.io/github/v/release/gojue/moling?display_name=tag&include_prereleases&sort=semver)](https://github.com/gojue/moling/releases)
  9 | 
 10 | ---
 11 | 
 12 | ![](./images/logo.svg)
 13 | 
 14 | ### Introduction
 15 | MoLing is a computer-use and browser-use MCP Server that implements system interaction through operating system APIs, enabling file system operations such as reading, writing, merging, statistics, and aggregation, as well as the ability to execute system commands. It is a dependency-free local office automation assistant.
 16 | 
 17 | ### Advantages
 18 | > [!IMPORTANT]
 19 | > Requiring no installation of any dependencies, MoLing can be run directly and is compatible with multiple operating systems, including Windows, Linux, and macOS. 
 20 | > This eliminates the hassle of dealing with environment conflicts involving Node.js, Python, Docker and other development environments.
 21 | 
 22 | ### Features
 23 | 
 24 | > [!CAUTION]
 25 | > Command-line operations are dangerous and should be used with caution.
 26 | 
 27 | - **File System Operations**: Reading, writing, merging, statistics, and aggregation
 28 | - **Command-line Terminal**: Execute system commands directly
 29 | - **Browser Control**: Powered by `github.com/chromedp/chromedp`
 30 |     - Chrome browser is required.
 31 |     - In Windows, the full path to Chrome needs to be configured in the system environment variables.
 32 | - **Future Plans**:
 33 |     - Personal PC data organization
 34 |     - Document writing assistance
 35 |     - Schedule planning
 36 |     - Life assistant features
 37 | 
 38 | > [!WARNING]
 39 | > Currently, MoLing has only been tested on macOS, and other operating systems may have issues.
 40 | 
 41 | ### Supported MCP Clients
 42 | 
 43 | - [Claude](https://claude.ai/)
 44 | - [Cline](https://cline.bot/)
 45 | - [Cherry Studio](https://cherry-ai.com/)
 46 | - etc. (who support MCP protocol)
 47 | 
 48 | #### Demos
 49 | 
 50 | https://github.com/user-attachments/assets/229c4dd5-23b4-4b53-9e25-3eba8734b5b7
 51 | 
 52 | MoLing in [Claude](https://claude.ai/)
 53 | ![](./images/screenshot_claude.png)
 54 | 
 55 | #### Configuration Format
 56 | 
 57 | ##### MCP Server (MoLing) configuration
 58 | 
 59 | The configuration file will be generated at `/Users/username/.moling/config/config.json`, and you can modify its
 60 | contents as needed.
 61 | 
 62 | If the file does not exist, you can create it using `moling config --init`.
 63 | 
 64 | ##### MCP Client configuration
 65 | For example, to configure the Claude client, add the following configuration:
 66 | 
 67 | > [!TIP]
 68 | > 
 69 | > Only 3-6 lines of configuration are needed.
 70 | > 
 71 | > Claude config path: `~/Library/Application\ Support/Claude/claude_desktop_config`
 72 | 
 73 | ```json
 74 | {
 75 |   "mcpServers": {
 76 |     "MoLing": {
 77 |       "command": "/usr/local/bin/moling",
 78 |       "args": []
 79 |     }
 80 |   }
 81 | }
 82 | ```
 83 | 
 84 | and, `/usr/local/bin/moling` is the path to the MoLing server binary you downloaded.
 85 | 
 86 | **Automatic Configuration**
 87 | 
 88 | run `moling client --install` to automatically install the configuration for the MCP client.
 89 | 
 90 | MoLing will automatically detect the MCP client and install the configuration for you. including: Cline, Claude, Roo
 91 | Code, etc.
 92 | 
 93 | ### Operation Modes
 94 | 
 95 | - **Stdio Mode**: CLI-based interactive mode for user-friendly experience
 96 | - **SSE Mode**: Server-Side Rendering mode optimized for headless/automated environments
 97 | 
 98 | ### Installation
 99 | 
100 | #### Option 1: Install via Script
101 | ##### Linux/MacOS
102 | ```shell
103 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.sh)"
104 | ```
105 | ##### Windows
106 | 
107 | > [!WARNING]
108 | > Not tested, unsure if it works.
109 | 
110 | ```powershell
111 | powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.ps1 | iex"
112 | ```
113 | 
114 | #### Option 2: Direct Download
115 | 1. Download the installation package from [releases page](https://github.com/gojue/moling/releases)
116 | 2. Extract the package
117 | 3. Run the server:
118 |    ```sh
119 |    ./moling
120 |    ```
121 | 
122 | #### Option 3: Build from Source
123 | 1. Clone the repository:
124 | ```sh
125 | git clone https://github.com/gojue/moling.git
126 | cd moling
127 | ```
128 | 2. Build the project (requires Golang toolchain):
129 | ```sh
130 | make build
131 | ```
132 | 3. Run the compiled binary:
133 | ```sh
134 | ./bin/moling
135 | ```
136 | 
137 | ### Usage
138 | After starting the server, connect using any supported MCP client by configuring it to point to your MoLing server address.
139 | 
140 | ### License
141 | Apache License 2.0. See [LICENSE](LICENSE) for details.
142 | 
```

--------------------------------------------------------------------------------
/cli/cmd/utils.go:
--------------------------------------------------------------------------------

```go
1 | package cmd
2 | 
```

--------------------------------------------------------------------------------
/functions.mk:
--------------------------------------------------------------------------------

```
 1 | define allow-override
 2 |   $(if $(or $(findstring environment,$(origin $(1))),\
 3 |             $(findstring command line,$(origin $(1)))),,\
 4 |     $(eval $(1) = $(2)))
 5 | endef
 6 | 
 7 | # TARGET_OS , TARGET_ARCH
 8 | define gobuild
 9 | 	CGO_ENABLED=0 \
10 | 	GOOS=$(1) GOARCH=$(2) \
11 | 	$(eval OUT_BIN_SUFFIX=$(if $(filter $(1),windows),.exe,)) \
12 | 	$(CMD_GO) build -trimpath -mod=readonly -ldflags "-w -s -X 'github.com/gojue/moling/cli/cmd.GitVersion=$(1)_$(2)_$(VERSION_NUM)'" -o $(OUT_BIN)$(OUT_BIN_SUFFIX)
13 | 	$(CMD_FILE) $(OUT_BIN)$(OUT_BIN_SUFFIX)
14 | endef
15 | 
16 | 
```

--------------------------------------------------------------------------------
/pkg/config/config_test.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "BrowserServer": {
 3 |   },
 4 |   "CommandServer": {
 5 |     "allowed_commands": [
 6 |       "ls",
 7 |       "cat",
 8 |       "echo"
 9 |     ]
10 |   },
11 |   "FilesystemServer": {
12 |     "allowed_dirs": [
13 |       "/tmp/.moling/data/"
14 |     ],
15 |     "cache_path": "/tmp/.moling/data"
16 |   },
17 |   "MoLingConfig": {
18 |     "HomeDir": "",
19 |     "SystemInfo": "",
20 |     "Username": "",
21 |     "base_path": "/newpath/.moling",
22 |     "config_file": "config/config.json",
23 |     "debug": false,
24 |     "listen_addr": "",
25 |     "version": "darwin-arm64-20250330084836-0077553"
26 |   },
27 |   "MoaServer": {}
28 | }
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package main
16 | 
17 | import "github.com/gojue/moling/cli"
18 | 
19 | func main() {
20 | 	cli.Start()
21 | }
22 | 
```

--------------------------------------------------------------------------------
/cli/main.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package cli
18 | 
19 | import (
20 | 	"github.com/gojue/moling/cli/cmd"
21 | )
22 | 
23 | func Start() {
24 | 	cmd.Execute()
25 | }
26 | 
```

--------------------------------------------------------------------------------
/pkg/comm/errors.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package comm
18 | 
19 | import "errors"
20 | 
21 | var (
22 | 	ErrConfigNotLoaded = errors.New("config not loaded, please call LoadConfig() first")
23 | )
24 | 
```

--------------------------------------------------------------------------------
/pkg/services/filesystem/file_system_windows.go:
--------------------------------------------------------------------------------

```go
 1 | /*
 2 |  * Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  *
16 |  * Repository: https://github.com/gojue/moling
17 |  */
18 | 
19 | package filesystem
20 | 
21 | import "os"
22 | 
23 | func init() {
24 | 	dirName, err := os.UserCacheDir()
25 | 	if err != nil {
26 | 		dirName, err = os.UserHomeDir()
27 | 		if err != nil {
28 | 			return
29 | 		}
30 | 	}
31 | 	allowedDirsDefault = dirName
32 | }
33 | 
```

--------------------------------------------------------------------------------
/pkg/services/command/command_exec_windows.go:
--------------------------------------------------------------------------------

```go
 1 | //go:build windows
 2 | 
 3 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 4 | //
 5 | // Licensed under the Apache License, Version 2.0 (the "License");
 6 | // you may not use this file except in compliance with the License.
 7 | // You may obtain a copy of the License at
 8 | //
 9 | //   http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 | //
17 | // Repository: https://github.com/gojue/moling
18 | 
19 | // Package services Description: This file contains the implementation of the CommandServer interface for Windows.
20 | package command
21 | 
22 | import (
23 | 	"os/exec"
24 | )
25 | 
26 | // ExecCommand executes a command and returns its output.
27 | func ExecCommand(command string) (string, error) {
28 | 	var cmd *exec.Cmd
29 | 	cmd = exec.Command("cmd", "/C", command)
30 | 	output, err := cmd.CombinedOutput()
31 | 	return string(output), err
32 | }
33 | 
```

--------------------------------------------------------------------------------
/.github/workflows/go-test.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Golang CI and Test
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ master ]
 6 |   pull_request:
 7 |     branches: [ master ]
 8 | 
 9 | jobs:
10 |   build-on-ubuntu2204:
11 |     strategy:
12 |       matrix:
13 |         os: [ darwin, windows, linux ]
14 |         arch: [amd64, arm64]
15 |     runs-on: ubuntu-22.04
16 |     name: test on ${{ matrix.os }} ${{ matrix.arch }}
17 |     steps:
18 |       - uses: actions/setup-go@v5
19 |         with:
20 |           go-version: '1.24.1'
21 |       - uses: actions/checkout@v4
22 |         with:
23 |           submodules: 'recursive'
24 |           fetch-depth: 0
25 |       - name: golangci-lint
26 |         uses: golangci/golangci-lint-action@v8
27 |         with:
28 |           version: v2.1
29 |           skip-cache: true
30 |           problem-matchers: true
31 |       - name: Test (go test)
32 |         run: |
33 |           make clean
34 |           TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env
35 |           TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make test
36 |       - name: MoLing Build
37 |         run: |
38 |           make clean
39 |           TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env
40 |           TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make build
```

--------------------------------------------------------------------------------
/pkg/utils/pid_unix.go:
--------------------------------------------------------------------------------

```go
 1 | //go:build darwin || linux || freebsd || openbsd || netbsd
 2 | 
 3 | /*
 4 |  * Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 5 |  *
 6 |  * Licensed under the Apache License, Version 2.0 (the "License");
 7 |  * you may not use this file except in compliance with the License.
 8 |  * You may obtain a copy of the License at
 9 |  *
10 |  * http://www.apache.org/licenses/LICENSE-2.0
11 |  *
12 |  * Unless required by applicable law or agreed to in writing, software
13 |  * distributed under the License is distributed on an "AS IS" BASIS,
14 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 |  * See the License for the specific language governing permissions and
16 |  * limitations under the License.
17 |  *
18 |  * Repository: https://github.com/gojue/moling
19 |  */
20 | 
21 | package utils
22 | 
23 | import (
24 | 	"errors"
25 | 	"os"
26 | 	"syscall"
27 | )
28 | 
29 | func lockFile(file *os.File) (bool, error) {
30 | 	err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
31 | 	if err != nil {
32 | 		if errors.Is(err, syscall.EWOULDBLOCK) {
33 | 			return false, nil // 已经被锁定,返回false但不返回错误
34 | 		}
35 | 		return false, err
36 | 	}
37 | 	return true, nil
38 | }
39 | 
40 | func unlockFile(file *os.File) error {
41 | 	return syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
42 | }
43 | 
```

--------------------------------------------------------------------------------
/cli/cmd/perrun.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package cmd
18 | 
19 | import (
20 | 	"path/filepath"
21 | 
22 | 	"github.com/spf13/cobra"
23 | 
24 | 	"github.com/gojue/moling/pkg/utils"
25 | )
26 | 
27 | // mlsCommandPreFunc is a pre-run function for the MoLing command.
28 | func mlsCommandPreFunc(cmd *cobra.Command, args []string) error {
29 | 	err := utils.CreateDirectory(mlConfig.BasePath)
30 | 	if err != nil {
31 | 		return err
32 | 	}
33 | 	for _, dirName := range mlDirectories {
34 | 		err = utils.CreateDirectory(filepath.Join(mlConfig.BasePath, dirName))
35 | 		if err != nil {
36 | 			return err
37 | 		}
38 | 	}
39 | 	return nil
40 | }
41 | 
```

--------------------------------------------------------------------------------
/pkg/services/command/command_exec.go:
--------------------------------------------------------------------------------

```go
 1 | //go:build !windows
 2 | 
 3 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 4 | //
 5 | // Licensed under the Apache License, Version 2.0 (the "License");
 6 | // you may not use this file except in compliance with the License.
 7 | // You may obtain a copy of the License at
 8 | //
 9 | //   http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 | //
17 | // Repository: https://github.com/gojue/moling
18 | 
19 | package command
20 | 
21 | import (
22 | 	"context"
23 | 	"errors"
24 | 	"os/exec"
25 | 	"time"
26 | )
27 | 
28 | // ExecCommand executes a command and returns its output.
29 | func ExecCommand(command string) (string, error) {
30 | 	var cmd *exec.Cmd
31 | 	ctx, cfunc := context.WithTimeout(context.Background(), time.Second*10)
32 | 	defer cfunc()
33 | 	cmd = exec.CommandContext(ctx, "sh", "-c", command)
34 | 	output, err := cmd.CombinedOutput()
35 | 	if err != nil {
36 | 		switch {
37 | 		case errors.Is(err, exec.ErrNotFound):
38 | 			// 命令未找到
39 | 			return "", errors.New("command not found")
40 | 		case errors.Is(ctx.Err(), context.DeadlineExceeded):
41 | 			// 超时时仅返回输出,不返回错误
42 | 			return string(output), nil
43 | 		default:
44 | 			return string(output), nil
45 | 		}
46 | 	}
47 | 
48 | 	return string(output), nil
49 | }
50 | 
```

--------------------------------------------------------------------------------
/pkg/services/browser/browser_test.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package browser
18 | 
19 | import (
20 | 	"testing"
21 | 
22 | 	"github.com/gojue/moling/pkg/comm"
23 | )
24 | 
25 | func TestBrowserServer(t *testing.T) {
26 | 	//
27 | 	//cfg := &BrowserConfig{
28 | 	//	Headless:        true,
29 | 	//	Timeout:         30,
30 | 	//	UserAgent:       "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
31 | 	//	DefaultLanguage: "en-US",
32 | 	//	URLTimeout:      10,
33 | 	//	SelectorQueryTimeout:      10,
34 | 	//}
35 | 	logger, ctx, err := comm.InitTestEnv()
36 | 	if err != nil {
37 | 		t.Fatalf("Failed to initialize test environment: %s", err.Error())
38 | 	}
39 | 	logger.Info().Msg("TestBrowserServer")
40 | 
41 | 	_, err = NewBrowserServer(ctx)
42 | 	if err != nil {
43 | 		t.Fatalf("Failed to create BrowserServer: %s", err.Error())
44 | 	}
45 | }
46 | 
```

--------------------------------------------------------------------------------
/variables.mk:
--------------------------------------------------------------------------------

```
 1 | CMD_MAKE = make
 2 | CMD_TR ?= tr
 3 | CMD_CUT ?= cut
 4 | CMD_AWK ?= awk
 5 | CMD_SED ?= sed
 6 | CMD_FILE ?= file
 7 | CMD_GIT ?= git
 8 | CMD_CLANG ?= clang
 9 | CMD_RM ?= rm
10 | CMD_INSTALL ?= install
11 | CMD_MKDIR ?= mkdir
12 | CMD_TOUCH ?= touch
13 | CMD_GO ?= go
14 | CMD_GREP ?= grep
15 | CMD_CAT ?= cat
16 | CMD_MD5 ?= md5sum
17 | CMD_TAR ?= tar
18 | CMD_CHECKSUM ?= sha256sum
19 | CMD_GITHUB ?= gh
20 | CMD_MV ?= mv
21 | CMD_CP ?= cp
22 | CMD_CD ?= cd
23 | CMD_ECHO ?= echo
24 | 
25 | 
26 | #
27 | # tools version
28 | #
29 | 
30 | GO_VERSION = $(shell $(CMD_GO) version 2>/dev/null | $(CMD_AWK) '{print $$3}' | $(CMD_SED) 's:go::g' | $(CMD_CUT) -d. -f1,2)
31 | GO_VERSION_MAJ = $(shell $(CMD_ECHO) $(GO_VERSION) | $(CMD_CUT) -d'.' -f1)
32 | GO_VERSION_MIN = $(shell $(CMD_ECHO) $(GO_VERSION) | $(CMD_CUT) -d'.' -f2)
33 | 
34 | # tags date info
35 | VERSION_NUM ?= v0.0.0
36 | NOW_DATE := $(shell date +"%Y-%m-%d %H:%M:%S")
37 | TAG_COMMIT := $(shell git rev-list --abbrev-commit --tags --max-count=1)
38 | TAG := $(shell git describe --abbrev=0 --tags ${TAG_COMMIT} 2>/dev/null || true)
39 | COMMIT := $(shell git rev-parse --short HEAD)
40 | DATE := $(shell git log -1 --format=%cd --date=format:"%Y %m %d")
41 | LAST_GIT_TAG := $(COMMIT)_$(NOW_DATE)
42 | 
43 | ifndef SNAPSHOT_VERSION
44 | 	VERSION_NUM = $(LAST_GIT_TAG)
45 | else
46 | 	VERSION_NUM = $(SNAPSHOT_VERSION)_$(NOW_DATE)
47 | endif
48 | 
49 | #
50 | # environment
51 | #
52 | #SNAPSHOT_VERSION ?= $(shell git rev-parse HEAD)
53 | BUILD_DATE := $(shell date +%Y-%m-%d)
54 | TARGET_TAG :=
55 | OS_NAME ?= $(shell uname -s|tr 'A-Z' 'a-z')
56 | OS_ARCH ?= $(shell uname -m)
57 | OS_VERSION_SHORT := $(shell uname -r | cut -d'-' -f 1)
58 | TARGET_OS ?= $(OS_NAME)
59 | TARGET_ARCH ?= $(if $(filter x86_64,$(OS_ARCH)),amd64,arm64)
60 | OUT_BIN := bin/moling
```

--------------------------------------------------------------------------------
/install/install.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | set -e
 4 | 
 5 | echo "Welcome to MoLing MCP Server initialization script."
 6 | echo "Home page: https://gojue.cc/moling"
 7 | echo "Github: https://github.com/gojue/moling"
 8 | 
 9 | # Determine the OS and architecture
10 | OS=$(uname -s | tr '[:upper:]' '[:lower:]')
11 | ARCH=$(uname -m)
12 | 
13 | case $ARCH in
14 |   x86_64)
15 |     ARCH="amd64"
16 |     ;;
17 |   arm64|aarch64)
18 |     ARCH="arm64"
19 |     ;;
20 |   *)
21 |     echo "Unsupported architecture: $ARCH"
22 |     exit 1
23 |     ;;
24 | esac
25 | 
26 | # Determine the download URL
27 | VERSION=$(curl -s https://api.github.com/repos/gojue/moling/releases/latest | grep 'tag_name' | cut -d\" -f4)
28 | BASE_URL="https://github.com/gojue/moling/releases/download/${VERSION}"
29 | FILE_NAME="moling-${VERSION}-${OS}-${ARCH}.tar.gz"
30 | 
31 | DOWNLOAD_URL="${BASE_URL}/${FILE_NAME}"
32 | 
33 | # Download the installation package
34 | echo "Downloading ${DOWNLOAD_URL}..."
35 | curl -LO "${DOWNLOAD_URL}"
36 | echo "Download completed. filename: ${FILE_NAME}"
37 | # Extract the package
38 | tar -xzf "${FILE_NAME}"
39 | 
40 | # Move the binary to /usr/local/bin
41 | mv moling /usr/local/bin/moling
42 | chmod +x /usr/local/bin/moling
43 | 
44 | # Clean up
45 | rm -rf moling "${FILE_NAME}"
46 | 
47 | # Check if the installation was successful
48 | if command -v moling &> /dev/null; then
49 |     echo "MoLing installation was successful!"
50 | else
51 |     echo "MoLing installation failed."
52 |     exit 1
53 | fi
54 | 
55 | # initialize the configuration
56 | echo "Initializing MoLing configuration..."
57 | moling config --init
58 | echo "MoLing configuration initialized successfully!"
59 | 
60 | echo "setup MCP Server configuration into MCP Client"
61 | moling client -i
62 | echo "MCP Client configuration setup successfully!"
```

--------------------------------------------------------------------------------
/pkg/services/register.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package services
18 | 
19 | import (
20 | 	"github.com/gojue/moling/pkg/comm"
21 | 	"github.com/gojue/moling/pkg/services/abstract"
22 | 	"github.com/gojue/moling/pkg/services/browser"
23 | 	"github.com/gojue/moling/pkg/services/command"
24 | 	"github.com/gojue/moling/pkg/services/filesystem"
25 | )
26 | 
27 | var serviceLists = make(map[comm.MoLingServerType]abstract.ServiceFactory)
28 | 
29 | // RegisterServ register service
30 | func RegisterServ(n comm.MoLingServerType, f abstract.ServiceFactory) {
31 | 	serviceLists[n] = f
32 | }
33 | 
34 | // ServiceList  get service lists
35 | func ServiceList() map[comm.MoLingServerType]abstract.ServiceFactory {
36 | 	return serviceLists
37 | }
38 | 
39 | func init() {
40 | 	// Register the filesystem service
41 | 	RegisterServ(filesystem.FilesystemServerName, filesystem.NewFilesystemServer)
42 | 	// Register the browser service
43 | 	RegisterServ(browser.BrowserServerName, browser.NewBrowserServer)
44 | 	// Register the command service
45 | 	RegisterServ(command.CommandServerName, command.NewCommandServer)
46 | }
47 | 
```

--------------------------------------------------------------------------------
/install/install.ps1:
--------------------------------------------------------------------------------

```
 1 | #!/usr/bin/env pwsh
 2 | 
 3 | Set-StrictMode -Version Latest
 4 | 
 5 | Write-Output "Welcome to MoLing MCP Server initialization script."
 6 | Write-Output "Home page: https://gojue.cc/moling"
 7 | Write-Output "Github: https://github.com/gojue/moling"
 8 | 
 9 | # Determine the OS and architecture
10 | $OS = (Get-CimInstance Win32_OperatingSystem).Caption
11 | $ARCH = (Get-CimInstance Win32_Processor).Architecture
12 | 
13 | switch ($ARCH) {
14 |     9 { $ARCH = "amd64" }
15 |     5 { $ARCH = "arm64" }
16 |     default {
17 |         Write-Error "Unsupported architecture: $ARCH"
18 |         exit 1
19 |     }
20 | }
21 | 
22 | # Determine the download URL
23 | $VERSION = "v0.0.1"
24 | $BASE_URL = "https://github.com/gojue/moling/releases/download/$VERSION"
25 | $FILE_NAME = "moling-$VERSION-windows-$ARCH.zip"
26 | $DOWNLOAD_URL = "$BASE_URL/$FILE_NAME"
27 | 
28 | # Download the installation package
29 | Write-Output "Downloading $DOWNLOAD_URL..."
30 | Invoke-WebRequest -Uri $DOWNLOAD_URL -OutFile $FILE_NAME
31 | 
32 | # Extract the package
33 | Write-Output "Extracting $FILE_NAME..."
34 | Expand-Archive -Path $FILE_NAME -DestinationPath "moling"
35 | 
36 | # Move the binary to C:\Program Files
37 | $destination = "C:\Program Files\moling"
38 | if (-Not (Test-Path -Path $destination)) {
39 |     New-Item -ItemType Directory -Path $destination
40 | }
41 | Move-Item -Path "moling\moling.exe" -Destination "$destination\moling.exe"
42 | 
43 | # Add to PATH
44 | $env:Path += ";$destination"
45 | [System.Environment]::SetEnvironmentVariable("Path", $env:Path, [System.EnvironmentVariableTarget]::Machine)
46 | 
47 | # MCP Client configuration
48 | & "$destination\moling.exe" client --install
49 | 
50 | # Clean up
51 | Remove-Item -Recurse -Force "moling"
52 | Remove-Item -Force $FILE_NAME
53 | 
54 | Write-Output "MoLing has been installed successfully!"
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: MoLing Release
 2 | on:
 3 |   push:
 4 |     tags:
 5 |       - "v*"
 6 | 
 7 | concurrency:
 8 |   group: ${{ github.workflow }}-${{ github.ref }}
 9 |   cancel-in-progress: true
10 | 
11 | jobs:
12 |   build-on-ubuntu2204:
13 |     strategy:
14 |       matrix:
15 |         os: [ darwin, windows, linux ]
16 |         arch: [ amd64, arm64 ]
17 |     runs-on: ubuntu-22.04
18 |     name: Release on ${{ matrix.os }} ${{ matrix.arch }}
19 |     steps:
20 |       - uses: actions/setup-go@v5
21 |         with:
22 |           go-version: '1.24.1'
23 |       - uses: actions/checkout@v4
24 |         with:
25 |           submodules: 'recursive'
26 |           fetch-depth: 0
27 |       - name: MoLing Build
28 |         run: |
29 |           make clean
30 |           SNAPSHOT_VERSION=${{ github.ref_name }} TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env
31 |           SNAPSHOT_VERSION=${{ github.ref_name }} TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make build
32 |           pwd
33 |           ls -al ./bin
34 |       - name: Create Archive
35 |         run: |
36 |           mkdir -p ./dist
37 |           pwd
38 |           ls -al ./bin
39 |           if [ "${{ matrix.os }}" = "windows" ]; then
40 |             cd ./bin && zip -qr ./../dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.zip ./bin/ . && cd ..
41 |           else
42 |             tar -czvf dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz -C ./bin/ .
43 |           fi
44 |       - name: Upload Release Asset
45 |         uses: softprops/action-gh-release@v2
46 |         if: startsWith(github.ref, 'refs/tags/')
47 |         with:
48 |           tag_name: ${{ github.ref_name }}
49 |           generate_release_notes: true
50 |           files: |
51 |             ./dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.*
```

--------------------------------------------------------------------------------
/pkg/comm/comm.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package comm
18 | 
19 | import (
20 | 	"context"
21 | 	"os"
22 | 	"path/filepath"
23 | 
24 | 	"github.com/rs/zerolog"
25 | 
26 | 	"github.com/gojue/moling/pkg/config"
27 | )
28 | 
29 | type MoLingServerType string
30 | 
31 | type contextKey string
32 | 
33 | // MoLingConfigKey is a context key for storing the version of MoLing
34 | const (
35 | 	MoLingConfigKey contextKey = "moling_config"
36 | 	MoLingLoggerKey contextKey = "moling_logger"
37 | )
38 | 
39 | // InitTestEnv initializes the test environment by creating a temporary log file and setting up the logger.
40 | func InitTestEnv() (zerolog.Logger, context.Context, error) {
41 | 	logFile := filepath.Join(os.TempDir(), "moling.log")
42 | 	zerolog.SetGlobalLevel(zerolog.DebugLevel)
43 | 	var logger zerolog.Logger
44 | 	f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
45 | 	if err != nil {
46 | 		return zerolog.Logger{}, nil, err
47 | 	}
48 | 	logger = zerolog.New(f).With().Timestamp().Logger()
49 | 	mlConfig := &config.MoLingConfig{
50 | 		ConfigFile: filepath.Join("config", "test_config.json"),
51 | 		BasePath:   os.TempDir(),
52 | 	}
53 | 	ctx := context.WithValue(context.Background(), MoLingConfigKey, mlConfig)
54 | 	ctx = context.WithValue(ctx, MoLingLoggerKey, logger)
55 | 	return logger, ctx, nil
56 | }
57 | 
```

--------------------------------------------------------------------------------
/pkg/utils/pid_windows.go:
--------------------------------------------------------------------------------

```go
 1 | //go:build windows
 2 | 
 3 | /*
 4 |  * Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 5 |  *
 6 |  * Licensed under the Apache License, Version 2.0 (the "License");
 7 |  * you may not use this file except in compliance with the License.
 8 |  * You may obtain a copy of the License at
 9 |  *
10 |  * http://www.apache.org/licenses/LICENSE-2.0
11 |  *
12 |  * Unless required by applicable law or agreed to in writing, software
13 |  * distributed under the License is distributed on an "AS IS" BASIS,
14 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 |  * See the License for the specific language governing permissions and
16 |  * limitations under the License.
17 |  *
18 |  * Repository: https://github.com/gojue/moling
19 |  */
20 | 
21 | package utils
22 | 
23 | import (
24 | 	"errors"
25 | 	"os"
26 | 	"syscall"
27 | 	"unsafe"
28 | )
29 | 
30 | var (
31 | 	kernel32     = syscall.NewLazyDLL("kernel32.dll")
32 | 	lockFileEx   = kernel32.NewProc("LockFileEx")
33 | 	unlockFileEx = kernel32.NewProc("UnlockFileEx")
34 | )
35 | 
36 | const (
37 | 	LockfileExclusiveLock   = 2
38 | 	LockfileFailImmediately = 1
39 | )
40 | const ErrorLockViolation = syscall.Errno(33) // 0x21
41 | 
42 | // lockFile locks the given file using Windows API.
43 | func lockFile(file *os.File) (bool, error) {
44 | 	handle := syscall.Handle(file.Fd())
45 | 	var overlapped syscall.Overlapped
46 | 
47 | 	flags := LockfileExclusiveLock | LockfileFailImmediately
48 | 	r, _, err := lockFileEx.Call(
49 | 		uintptr(handle),
50 | 		uintptr(flags),
51 | 		0,
52 | 		1,
53 | 		0,
54 | 		uintptr(unsafe.Pointer(&overlapped)),
55 | 	)
56 | 
57 | 	if r == 0 {
58 | 		if !errors.Is(err, ErrorLockViolation) {
59 | 			return false, err
60 | 		}
61 | 		return false, nil
62 | 	}
63 | 
64 | 	return true, nil
65 | }
66 | 
67 | // unlockFile unlocks the given file using Windows API.
68 | func unlockFile(file *os.File) error {
69 | 	handle := syscall.Handle(file.Fd())
70 | 	var overlapped syscall.Overlapped
71 | 
72 | 	r, _, err := unlockFileEx.Call(
73 | 		uintptr(handle),
74 | 		0,
75 | 		1,
76 | 		0,
77 | 		uintptr(unsafe.Pointer(&overlapped)),
78 | 	)
79 | 
80 | 	if r == 0 {
81 | 		return err
82 | 	}
83 | 
84 | 	return nil
85 | }
86 | 
```

--------------------------------------------------------------------------------
/pkg/config/config_test.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package config
18 | 
19 | import (
20 | 	"encoding/json"
21 | 	"os"
22 | 	"testing"
23 | 
24 | 	"github.com/gojue/moling/pkg/utils"
25 | )
26 | 
27 | // TestConfigLoad tests the loading of the configuration from a JSON file.
28 | func TestConfigLoad(t *testing.T) {
29 | 	configFile := "config_test.json"
30 | 	cfg := &MoLingConfig{}
31 | 	cfg.ConfigFile = "config.json"
32 | 	cfg.BasePath = "/tmp/moling"
33 | 	cfg.Version = "1.0.0"
34 | 	cfg.ListenAddr = ":8080"
35 | 	cfg.Debug = true
36 | 	cfg.Username = "user1"
37 | 	cfg.HomeDir = "/Users/user1"
38 | 	cfg.SystemInfo = "Darwin 15.3.3"
39 | 
40 | 	jsonData, err := os.ReadFile(configFile)
41 | 	if err != nil {
42 | 		t.Fatalf("failed to read config file: %s", err.Error())
43 | 	}
44 | 	var jsonMap map[string]any
45 | 	if err := json.Unmarshal(jsonData, &jsonMap); err != nil {
46 | 		t.Fatalf("Failed to unmarshal JSON: %s", err.Error())
47 | 	}
48 | 	mlConfig, ok := jsonMap["MoLingConfig"].(map[string]any)
49 | 	if !ok {
50 | 		t.Fatalf("failed to parse MoLingConfig from JSON")
51 | 	}
52 | 	if err := utils.MergeJSONToStruct(cfg, mlConfig); err != nil {
53 | 		t.Fatalf("failed to merge JSON to struct: %s", err.Error())
54 | 	}
55 | 	t.Logf("Config loaded, MoLing Config.BasePath: %s", cfg.BasePath)
56 | 	if cfg.BasePath != "/newpath/.moling" {
57 | 		t.Fatalf("expected BasePath to be '/newpath/.moling', got '%s'", cfg.BasePath)
58 | 	}
59 | }
60 | 
```

--------------------------------------------------------------------------------
/client/client_config_windows.go:
--------------------------------------------------------------------------------

```go
 1 | /*
 2 |  *
 3 |  *  Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 4 |  *
 5 |  *  Licensed under the Apache License, Version 2.0 (the "License");
 6 |  *  you may not use this file except in compliance with the License.
 7 |  *  You may obtain a copy of the License at
 8 |  *
 9 |  *    http://www.apache.org/licenses/LICENSE-2.0
10 |  *
11 |  *  Unless required by applicable law or agreed to in writing, software
12 |  *  distributed under the License is distributed on an "AS IS" BASIS,
13 |  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 |  *  See the License for the specific language governing permissions and
15 |  *  limitations under the License.
16 |  *
17 |  *  Repository: https://github.com/gojue/moling
18 |  *
19 |  */
20 | 
21 | package client
22 | 
23 | import (
24 | 	"os"
25 | 	"path/filepath"
26 | )
27 | 
28 | func init() {
29 | 	clientLists["VSCODE Cline"] = filepath.Join(os.Getenv("APPDATA"), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
30 | 	clientLists["Trae CN Cline"] = filepath.Join(os.Getenv("APPDATA"), "Trae CN", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
31 | 	clientLists["Trae Cline"] = filepath.Join(os.Getenv("APPDATA"), "Trae", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
32 | 	clientLists["Trae"] = filepath.Join(os.Getenv("APPDATA"), "Trae", "User", "mcp.json")
33 | 	clientLists["Trae CN"] = filepath.Join(os.Getenv("APPDATA"), "Trae CN", "User", "mcp.json")
34 | 	clientLists["VSCODE Roo Code"] = filepath.Join(os.Getenv("APPDATA"), "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
35 | 	clientLists["Trae CN Roo"] = filepath.Join(os.Getenv("APPDATA"), "Trae CN", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "mcp_settings.json")
36 | 	clientLists["Trae Roo"] = filepath.Join(os.Getenv("APPDATA"), "Trae", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "mcp_settings.json")
37 | 	clientLists["Claude"] = filepath.Join(os.Getenv("APPDATA"), "Claude", "claude_desktop_config.json")
38 | 	clientLists["Cursor"] = filepath.Join(os.Getenv("APPDATA"), "Cursor", "mcp.json")
39 | }
40 | 
```

--------------------------------------------------------------------------------
/pkg/services/abstract/abstract.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //	http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package abstract
18 | 
19 | import (
20 | 	"context"
21 | 
22 | 	"github.com/mark3labs/mcp-go/mcp"
23 | 	"github.com/mark3labs/mcp-go/server"
24 | 
25 | 	"github.com/gojue/moling/pkg/comm"
26 | 	"github.com/gojue/moling/pkg/config"
27 | )
28 | 
29 | type ServiceFactory func(ctx context.Context) (Service, error)
30 | 
31 | // Service defines the interface for a service with various handlers and tools.
32 | type Service interface {
33 | 	Ctx() context.Context
34 | 	// Resources returns a map of resources and their corresponding handler functions.
35 | 	Resources() map[mcp.Resource]server.ResourceHandlerFunc
36 | 	// ResourceTemplates returns a map of resource templates and their corresponding handler functions.
37 | 	ResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc
38 | 	// Prompts returns a map of prompts and their corresponding handler functions.
39 | 	Prompts() []PromptEntry
40 | 	// Tools returns a slice of server tools.
41 | 	Tools() []server.ServerTool
42 | 	// NotificationHandlers returns a map of notification handlers.
43 | 	NotificationHandlers() map[string]server.NotificationHandlerFunc
44 | 
45 | 	// Config returns the configuration of the service as a string.
46 | 	Config() string
47 | 	// LoadConfig loads the configuration for the service from a map.
48 | 	LoadConfig(jsonData map[string]any) error
49 | 
50 | 	// Init initializes the service with the given context and configuration.
51 | 	Init() error
52 | 
53 | 	MlConfig() *config.MoLingConfig
54 | 
55 | 	// Name returns the name of the service.
56 | 	Name() comm.MoLingServerType
57 | 
58 | 	// Close closes the service and releases any resources it holds.
59 | 	Close() error
60 | }
61 | 
```

--------------------------------------------------------------------------------
/pkg/utils/pid.go:
--------------------------------------------------------------------------------

```go
 1 | /*
 2 |  * Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  *
16 |  * Repository: https://github.com/gojue/moling
17 |  */
18 | 
19 | package utils
20 | 
21 | import (
22 | 	"fmt"
23 | 	"os"
24 | )
25 | 
26 | var pidFile *os.File
27 | 
28 | // CreatePIDFile creates and locks a PID file to prevent multiple instances.
29 | func CreatePIDFile(pidFilePath string) error {
30 | 	// Open or create the PID file
31 | 	file, err := os.OpenFile(pidFilePath, os.O_RDWR|os.O_CREATE, 0644)
32 | 	if err != nil {
33 | 		return fmt.Errorf("failed to open PID file: %w", err)
34 | 	}
35 | 
36 | 	// Try to lock the file using platform-specific code
37 | 	locked, err := lockFile(file)
38 | 	if err != nil {
39 | 		_ = file.Close()
40 | 		return fmt.Errorf("failed to lock PID file: %w", err)
41 | 	}
42 | 	if !locked {
43 | 		_ = file.Close()
44 | 		return fmt.Errorf("another instance is already running: %s", pidFilePath)
45 | 	}
46 | 
47 | 	// Write the current PID to the file
48 | 	err = file.Truncate(0)
49 | 	if err != nil {
50 | 		_ = unlockFile(file)
51 | 		_ = file.Close()
52 | 		return fmt.Errorf("failed to truncate PID file: %w", err)
53 | 	}
54 | 	_, err = file.WriteString(fmt.Sprintf("%d\n", os.Getpid()))
55 | 	if err != nil {
56 | 		_ = unlockFile(file)
57 | 		_ = file.Close()
58 | 		return fmt.Errorf("failed to write PID to file: %w", err)
59 | 	}
60 | 
61 | 	// Keep the file open to maintain the lock
62 | 	pidFile = file
63 | 	return nil
64 | }
65 | 
66 | // RemovePIDFile releases the lock and removes the PID file.
67 | func RemovePIDFile(pidFilePath string) error {
68 | 	if pidFile != nil {
69 | 		err := unlockFile(pidFile)
70 | 		if err != nil {
71 | 			return fmt.Errorf("failed to unlock PID file: %w", err)
72 | 		}
73 | 		_ = pidFile.Close()
74 | 		pidFile = nil
75 | 		err = os.Remove(pidFilePath)
76 | 		if err != nil {
77 | 			return fmt.Errorf("failed to remove PID file: %w", err)
78 | 		}
79 | 	}
80 | 	return nil
81 | }
82 | 
```

--------------------------------------------------------------------------------
/pkg/server/server_test.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package server
18 | 
19 | import (
20 | 	"os"
21 | 	"path/filepath"
22 | 	"testing"
23 | 
24 | 	"github.com/gojue/moling/pkg/comm"
25 | 	"github.com/gojue/moling/pkg/config"
26 | 	"github.com/gojue/moling/pkg/services/abstract"
27 | 	"github.com/gojue/moling/pkg/services/filesystem"
28 | 	"github.com/gojue/moling/pkg/utils"
29 | )
30 | 
31 | func TestNewMLServer(t *testing.T) {
32 | 	// Create a new MoLingConfig
33 | 	mlConfig := config.MoLingConfig{
34 | 		BasePath: filepath.Join(os.TempDir(), "moling_test"),
35 | 	}
36 | 	mlDirectories := []string{
37 | 		"logs",    // log file
38 | 		"config",  // config file
39 | 		"browser", // browser cache
40 | 		"data",    // data
41 | 		"cache",
42 | 	}
43 | 	err := utils.CreateDirectory(mlConfig.BasePath)
44 | 	if err != nil {
45 | 		t.Errorf("Failed to create base directory: %s", err.Error())
46 | 	}
47 | 	for _, dirName := range mlDirectories {
48 | 		err = utils.CreateDirectory(filepath.Join(mlConfig.BasePath, dirName))
49 | 		if err != nil {
50 | 			t.Errorf("Failed to create directory %s: %s", dirName, err.Error())
51 | 		}
52 | 	}
53 | 	logger, ctx, err := comm.InitTestEnv()
54 | 	if err != nil {
55 | 		t.Fatalf("Failed to initialize test environment: %s", err.Error())
56 | 	}
57 | 	logger.Info().Msg("TestBrowserServer")
58 | 	mlConfig.SetLogger(logger)
59 | 
60 | 	// Create a new server with the filesystem service
61 | 	fs, err := filesystem.NewFilesystemServer(ctx)
62 | 	if err != nil {
63 | 		t.Errorf("Failed to create filesystem server: %s", err.Error())
64 | 	}
65 | 	err = fs.Init()
66 | 	if err != nil {
67 | 		t.Errorf("Failed to initialize filesystem server: %s", err.Error())
68 | 	}
69 | 	srvs := []abstract.Service{
70 | 		fs,
71 | 	}
72 | 	srv, err := NewMoLingServer(ctx, srvs, mlConfig)
73 | 	if err != nil {
74 | 		t.Errorf("Failed to create server: %s", err.Error())
75 | 	}
76 | 	err = srv.Serve()
77 | 	if err != nil {
78 | 		t.Errorf("Failed to start server: %s", err.Error())
79 | 	}
80 | 	t.Logf("Server started successfully: %v", srv)
81 | }
82 | 
```

--------------------------------------------------------------------------------
/client/client_config.go:
--------------------------------------------------------------------------------

```go
 1 | //go:build !windows
 2 | 
 3 | /*
 4 |  *
 5 |  *  Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 6 |  *
 7 |  *  Licensed under the Apache License, Version 2.0 (the "License");
 8 |  *  you may not use this file except in compliance with the License.
 9 |  *  You may obtain a copy of the License at
10 |  *
11 |  *    http://www.apache.org/licenses/LICENSE-2.0
12 |  *
13 |  *  Unless required by applicable law or agreed to in writing, software
14 |  *  distributed under the License is distributed on an "AS IS" BASIS,
15 |  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 |  *  See the License for the specific language governing permissions and
17 |  *  limitations under the License.
18 |  *
19 |  *  Repository: https://github.com/gojue/moling
20 |  *
21 |  */
22 | 
23 | package client
24 | 
25 | import (
26 | 	"os"
27 | 	"path/filepath"
28 | )
29 | 
30 | func init() {
31 | 	clientLists["VSCode Cline"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
32 | 	clientLists["Trae CN Cline"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae CN", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
33 | 	clientLists["Trae Cline"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
34 | 	clientLists["Trae"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae", "User", "mcp.json")
35 | 	clientLists["Trae CN"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae CN", "User", "mcp.json")
36 | 	clientLists["VSCode Roo"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
37 | 	clientLists["Trae CN Roo"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae CN", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
38 | 	clientLists["Trae Roo"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
39 | 	clientLists["Claude"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Claude", "claude_desktop_config.json")
40 | 	clientLists["Cursor"] = filepath.Join(os.Getenv("HOME"), ".cursor", "mcp.json")
41 | }
42 | 
```

--------------------------------------------------------------------------------
/pkg/config/config.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //   http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | //
15 | // Repository: https://github.com/gojue/moling
16 | 
17 | package config
18 | 
19 | import (
20 | 	"github.com/rs/zerolog"
21 | )
22 | 
23 | // Config is an interface that defines a method for checking configuration validity.
24 | type Config interface {
25 | 	// Check validates the configuration and returns an error if the configuration is invalid.
26 | 	Check() error
27 | }
28 | 
29 | // MoLingConfig is a struct that holds the configuration for the MoLing server.
30 | type MoLingConfig struct {
31 | 	ConfigFile string `json:"config_file"` // The path to the configuration file.
32 | 	BasePath   string `json:"base_path"`   // The base path for the server, used for storing files. automatically created if not exists. eg: /Users/user1/.moling
33 | 	//AllowDir   []string `json:"allow_dir"`   // The directories that are allowed to be accessed by the server.
34 | 	Version    string `json:"version"`     // The version of the MoLing server.
35 | 	ListenAddr string `json:"listen_addr"` // The address to listen on for SSE mode.
36 | 	Debug      bool   `json:"debug"`       // Debug mode, if true, the server will run in debug mode.
37 | 	Module     string `json:"module"`      // The module to load, default: all
38 | 	Username   string // The username of the user running the server.
39 | 	HomeDir    string // The home directory of the user running the server. macOS: /Users/user1, Linux: /home/user1
40 | 	SystemInfo string // The system information of the user running the server. macOS: Darwin 15.3.3, Linux: Ubuntu 20.04.1 LTS
41 | 
42 | 	// for MCP Server Config
43 | 	Description string // Description of the MCP Server, default: CliDescription
44 | 	Command     string //	Command to start the MCP Server, STDIO mode only,  default: CliName
45 | 	Args        string // Arguments to pass to the command, STDIO mode only, default: empty
46 | 	BaseURL     string // BaseURL , SSE mode only.
47 | 	ServerName  string // ServerName MCP ServerName, add to the MCP Client config
48 | 	logger      zerolog.Logger
49 | }
50 | 
51 | func (cfg *MoLingConfig) Check() error {
52 | 	panic("not implemented yet") // TODO: Implement Check
53 | }
54 | 
55 | func (cfg *MoLingConfig) Logger() zerolog.Logger {
56 | 	return cfg.logger
57 | }
58 | 
59 | func (cfg *MoLingConfig) SetLogger(logger zerolog.Logger) {
60 | 	cfg.logger = logger
61 | }
62 | 
```

--------------------------------------------------------------------------------
/cli/cmd/client.go:
--------------------------------------------------------------------------------

```go
 1 | /*
 2 |  *
 3 |  *  Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
 4 |  *
 5 |  *  Licensed under the Apache License, Version 2.0 (the "License");
 6 |  *  you may not use this file except in compliance with the License.
 7 |  *  You may obtain a copy of the License at
 8 |  *
 9 |  *    http://www.apache.org/licenses/LICENSE-2.0
10 |  *
11 |  *  Unless required by applicable law or agreed to in writing, software
12 |  *  distributed under the License is distributed on an "AS IS" BASIS,
13 |  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 |  *  See the License for the specific language governing permissions and
15 |  *  limitations under the License.
16 |  *
17 |  *  Repository: https://github.com/gojue/moling
18 |  *
19 |  */
20 | 
21 | package cmd
22 | 
23 | import (
24 | 	"os"
25 | 	"time"
26 | 
27 | 	"github.com/rs/zerolog"
28 | 	"github.com/spf13/cobra"
29 | 
30 | 	"github.com/gojue/moling/client"
31 | )
32 | 
33 | var clientCmd = &cobra.Command{
34 | 	Use:   "client",
35 | 	Short: "Provides automated access to MoLing MCP Server for local MCP clients, Cline, Roo Code, and Claude, etc.",
36 | 	Long: `Automatically checks the MCP clients installed on the current computer, displays them, and automatically adds the MoLing MCP Server configuration to enable one-click activation, reducing the hassle of manual configuration.
37 | Currently supports the following clients: Cline, Roo Code, Claude
38 |     moling client -l --list   List the current installed MCP clients
39 |     moling client -i --install Add MoLing MCP Server configuration to the currently installed MCP clients on this computer
40 | `,
41 | 	RunE: ClientCommandFunc,
42 | }
43 | 
44 | var (
45 | 	list    bool
46 | 	install bool
47 | )
48 | 
49 | // ClientCommandFunc executes the "config" command.
50 | func ClientCommandFunc(command *cobra.Command, args []string) error {
51 | 	logger := initLogger(mlConfig.BasePath)
52 | 	consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
53 | 	multi := zerolog.MultiLevelWriter(consoleWriter, logger)
54 | 	logger = zerolog.New(multi).With().Timestamp().Logger()
55 | 	mlConfig.SetLogger(logger)
56 | 	logger.Debug().Msg("Start to show MCP Clients")
57 | 	mcpConfig := client.NewMCPServerConfig(CliDescription, CliName, MCPServerName)
58 | 	exePath, err := os.Executable()
59 | 	if err == nil {
60 | 		logger.Debug().Str("exePath", exePath).Msg("executable path, will use this path to find the config file")
61 | 		mcpConfig.Command = exePath
62 | 	}
63 | 	cm := client.NewManager(logger, mcpConfig)
64 | 	if install {
65 | 		logger.Info().Msg("Start to add MCP Server configuration into MCP Clients.")
66 | 		cm.SetupConfig()
67 | 		logger.Info().Msg("Add MCP Server configuration into MCP Clients successfully.")
68 | 		return nil
69 | 	}
70 | 	logger.Info().Msg("Start to list MCP Clients")
71 | 	cm.ListClient()
72 | 	logger.Info().Msg("List MCP Clients successfully.")
73 | 	return nil
74 | }
75 | 
76 | func init() {
77 | 	clientCmd.PersistentFlags().BoolVar(&list, "list", false, "List the current installed MCP clients")
78 | 	clientCmd.PersistentFlags().BoolVarP(&install, "install", "i", false, "Add MoLing MCP Server configuration to the currently installed MCP clients on this computer. default is all")
79 | 	rootCmd.AddCommand(clientCmd)
80 | }
81 | 
```

--------------------------------------------------------------------------------
/pkg/services/command/command_exec_test.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | package command
 18 | 
 19 | import (
 20 | 	"context"
 21 | 	"errors"
 22 | 	"os/exec"
 23 | 	"reflect"
 24 | 	"testing"
 25 | 	"time"
 26 | 
 27 | 	"github.com/gojue/moling/pkg/comm"
 28 | )
 29 | 
 30 | // MockCommandServer is a mock implementation of CommandServer for testing purposes.
 31 | type MockCommandServer struct {
 32 | 	CommandServer
 33 | }
 34 | 
 35 | // TestExecuteCommand tests the ExecCommand function.
 36 | func TestExecuteCommand(t *testing.T) {
 37 | 	execCmd := "echo 'Hello, World!'"
 38 | 	// Test a simple command
 39 | 	output, err := ExecCommand(execCmd)
 40 | 	if err != nil {
 41 | 		t.Fatalf("Expected no error, got %v", err)
 42 | 	}
 43 | 	expectedOutput := "Hello, World!\n"
 44 | 	if output != expectedOutput {
 45 | 		t.Errorf("Expected output %q, got %q", expectedOutput, output)
 46 | 	}
 47 | 	t.Logf("Command output: %s", output)
 48 | 	// Test a command with a timeout
 49 | 	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*10)
 50 | 	defer cancel()
 51 | 
 52 | 	execCmd = "curl ifconfig.me|grep Time"
 53 | 	output, err = ExecCommand(execCmd)
 54 | 	if err != nil {
 55 | 		t.Fatalf("Expected no error, got %v", err)
 56 | 	}
 57 | 	t.Logf("Command output: %s", output)
 58 | 	cmd := exec.CommandContext(ctx, "sleep", "1")
 59 | 	err = cmd.Run()
 60 | 	if err == nil {
 61 | 		t.Fatalf("Expected timeout error, got nil")
 62 | 	}
 63 | 	if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
 64 | 		t.Errorf("Expected context deadline exceeded error, got %v", ctx.Err())
 65 | 	}
 66 | }
 67 | 
 68 | func TestAllowCmd(t *testing.T) {
 69 | 	// Test with a command that is allowed
 70 | 	_, ctx, err := comm.InitTestEnv()
 71 | 	if err != nil {
 72 | 		t.Fatalf("Failed to initialize test environment: %v", err)
 73 | 	}
 74 | 
 75 | 	cs, err := NewCommandServer(ctx)
 76 | 	if err != nil {
 77 | 		t.Fatalf("Failed to create CommandServer: %v", err)
 78 | 	}
 79 | 
 80 | 	cc := StructToMap(NewCommandConfig())
 81 | 	t.Logf("CommandConfig: %v", cc)
 82 | 	err = cs.LoadConfig(cc)
 83 | 	if err != nil {
 84 | 		t.Fatalf("Failed to load config: %v", err)
 85 | 	}
 86 | 	cmd := "cd /var/logs/notfound && git log --since=\"today\" --pretty=format:\"%h - %an, %ar : %s\""
 87 | 	cs1 := cs.(*CommandServer)
 88 | 	if !cs1.isAllowedCommand(cmd) {
 89 | 		t.Errorf("Command '%s' is not allowed", cmd)
 90 | 	}
 91 | 	t.Log("Command is allowed:", cmd)
 92 | }
 93 | 
 94 | // 将 struct 转换为 map
 95 | func StructToMap(obj any) map[string]any {
 96 | 	result := make(map[string]any)
 97 | 	val := reflect.ValueOf(obj)
 98 | 	if val.Kind() == reflect.Ptr {
 99 | 		val = val.Elem()
100 | 	}
101 | 	if val.Kind() != reflect.Struct {
102 | 		return nil
103 | 	}
104 | 	typ := val.Type()
105 | 	for i := 0; i < val.NumField(); i++ {
106 | 		field := typ.Field(i)
107 | 		value := val.Field(i)
108 | 		// 跳过未导出的字段
109 | 		if field.PkgPath != "" {
110 | 			continue
111 | 		}
112 | 		// 获取字段的 json tag(如果存在)
113 | 		key := field.Name
114 | 		if tag := field.Tag.Get("json"); tag != "" {
115 | 			key = tag
116 | 		}
117 | 		result[key] = value.Interface()
118 | 	}
119 | 	return result
120 | }
121 | 
```

--------------------------------------------------------------------------------
/client/client_test.go:
--------------------------------------------------------------------------------

```go
  1 | /*
  2 |  *
  3 |  *  Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  4 |  *
  5 |  *  Licensed under the Apache License, Version 2.0 (the "License");
  6 |  *  you may not use this file except in compliance with the License.
  7 |  *  You may obtain a copy of the License at
  8 |  *
  9 |  *    http://www.apache.org/licenses/LICENSE-2.0
 10 |  *
 11 |  *  Unless required by applicable law or agreed to in writing, software
 12 |  *  distributed under the License is distributed on an "AS IS" BASIS,
 13 |  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14 |  *  See the License for the specific language governing permissions and
 15 |  *  limitations under the License.
 16 |  *
 17 |  *  Repository: https://github.com/gojue/moling
 18 |  *
 19 |  */
 20 | 
 21 | package client
 22 | 
 23 | import (
 24 | 	"os"
 25 | 	"testing"
 26 | 
 27 | 	"github.com/rs/zerolog"
 28 | )
 29 | 
 30 | func TestClientManager_ListClient(t *testing.T) {
 31 | 	logger := zerolog.New(os.Stdout)
 32 | 	mcpConfig := NewMCPServerConfig("MoLing UnitTest Description", "moling_test", "MoLing MCP Server")
 33 | 	cm := NewManager(logger, mcpConfig)
 34 | 	// Mock client list
 35 | 	clientLists["TestClient"] = "/path/to/nonexistent/file"
 36 | 
 37 | 	cm.ListClient()
 38 | 	// Check logs or other side effects as needed
 39 | }
 40 | 
 41 | /*
 42 | 	func TestClientManager_SetupConfig(t *testing.T) {
 43 | 		logger := zerolog.New(os.Stdout)
 44 | 		mcpConfig := NewMCPServerConfig("MoLing UnitTest Description", "moling_test", "MoLing MCP Server")
 45 | 		cm := NewManager(logger, mcpConfig)
 46 | 
 47 | 		// Mock client list
 48 | 		clientLists["TestClient"] = "/path/to/nonexistent/file"
 49 | 
 50 | 		cm.SetupConfig()
 51 | 		// Check logs or other side effects as needed
 52 | 	}
 53 | 
 54 | 	func TestClientManager_appendConfig(t *testing.T) {
 55 | 		logger := zerolog.New(os.Stdout)
 56 | 		mcpConfig := NewMCPServerConfig("MoLing UnitTest Description", "moling_test", "MoLing MCP Server")
 57 | 		cm := NewManager(logger, mcpConfig)
 58 | 
 59 | 		// Mock payload
 60 | 		payload := []byte(`{
 61 | 	  "Cline": {
 62 | 	    "description": "MoLing UnitTest Description",
 63 | 	    "isActive": true,
 64 | 	    "command": "moling_test"
 65 | 	  },
 66 | 	  "mcpServers": {
 67 | 	    "testABC": {
 68 | 	      "args": [
 69 | 	        "--allow-dir",
 70 | 	        "/tmp/,/Users/username/Downloads"
 71 | 	      ],
 72 | 	      "command": "npx",
 73 | 	      "timeout": 300
 74 | 	    }
 75 | 	  }
 76 | 	}`)
 77 | 
 78 | 	result, err := cm.appendConfig("TestClient", payload)
 79 | 	if err != nil {
 80 | 		t.Fatalf("Expected no error, got %s", err.Error())
 81 | 	}
 82 | 
 83 | 	var resultMap map[string]any
 84 | 	err = json.Unmarshal(result, &resultMap)
 85 | 	if err != nil {
 86 | 		t.Fatalf("Expected valid JSON, got error %s", err.Error())
 87 | 	}
 88 | 
 89 | 	if resultMap["existingKey"] != "existingValue" {
 90 | 		t.Errorf("Expected existingKey to be existingValue, got %v", resultMap["existingKey"])
 91 | 	}
 92 | 
 93 | }
 94 | */
 95 | 
 96 | func TestClientManager_checkExist(t *testing.T) {
 97 | 	logger := zerolog.New(os.Stdout)
 98 | 	mcpConfig := NewMCPServerConfig("MoLing UnitTest Description", "moling_test", "MoLing MCP Server")
 99 | 	cm := NewManager(logger, mcpConfig)
100 | 
101 | 	// Test with a non-existent file
102 | 	exists := cm.checkExist("/path/to/nonexistent/file")
103 | 	if exists {
104 | 		t.Errorf("Expected file to not exist")
105 | 	}
106 | 
107 | 	// Test with an existing file
108 | 	file, err := os.CreateTemp("", "testfile")
109 | 	if err != nil {
110 | 		t.Fatalf("Failed to create temp file: %s", err.Error())
111 | 	}
112 | 	defer func() {
113 | 		_ = os.Remove(file.Name())
114 | 	}()
115 | 	t.Logf("Created temp file: %s", file.Name())
116 | 	exists = cm.checkExist(file.Name())
117 | 	if !exists {
118 | 		t.Errorf("Expected file to exist")
119 | 	}
120 | }
121 | 
```

--------------------------------------------------------------------------------
/pkg/utils/utils.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | package utils
 18 | 
 19 | import (
 20 | 	"fmt"
 21 | 	"mime"
 22 | 	"net/http"
 23 | 	"os"
 24 | 	"path/filepath"
 25 | 	"reflect"
 26 | 	"strings"
 27 | )
 28 | 
 29 | // CreateDirectory checks if a directory exists, and creates it if it doesn't
 30 | func CreateDirectory(path string) error {
 31 | 	_, err := os.Stat(path)
 32 | 	if err != nil {
 33 | 		if os.IsNotExist(err) {
 34 | 			err = os.MkdirAll(path, 0o755)
 35 | 			if err != nil {
 36 | 				return err
 37 | 			}
 38 | 		} else {
 39 | 			return err
 40 | 		}
 41 | 	}
 42 | 	return nil
 43 | }
 44 | 
 45 | // StringInSlice checks if a string is in a slice of strings
 46 | func StringInSlice(s string, modules []string) bool {
 47 | 	for _, module := range modules {
 48 | 		if module == s {
 49 | 			return true
 50 | 		}
 51 | 	}
 52 | 	return false
 53 | }
 54 | 
 55 | // MergeJSONToStruct 将JSON中的字段合并到结构体中
 56 | func MergeJSONToStruct(target any, jsonMap map[string]any) error {
 57 | 	// 获取目标结构体的反射值
 58 | 	val := reflect.ValueOf(target).Elem()
 59 | 	typ := val.Type()
 60 | 
 61 | 	// 遍历JSON map中的每个字段
 62 | 	for jsonKey, jsonValue := range jsonMap {
 63 | 		// 遍历结构体的每个字段
 64 | 		for i := 0; i < typ.NumField(); i++ {
 65 | 			field := typ.Field(i)
 66 | 			// 检查JSON字段名是否与结构体的JSON tag匹配
 67 | 			if field.Tag.Get("json") == jsonKey {
 68 | 				// 获取结构体字段的反射值
 69 | 				fieldVal := val.Field(i)
 70 | 				// 检查字段是否可设置
 71 | 				if fieldVal.CanSet() {
 72 | 					// 将JSON值转换为结构体字段的类型
 73 | 					jsonVal := reflect.ValueOf(jsonValue)
 74 | 					if jsonVal.Type().ConvertibleTo(fieldVal.Type()) {
 75 | 						fieldVal.Set(jsonVal.Convert(fieldVal.Type()))
 76 | 					} else {
 77 | 						return fmt.Errorf("type mismatch for field %s, value:%v", jsonKey, jsonValue)
 78 | 					}
 79 | 				}
 80 | 			}
 81 | 		}
 82 | 	}
 83 | 	return nil
 84 | }
 85 | 
 86 | // DetectMimeType tries to determine the MIME type of a file
 87 | func DetectMimeType(path string) string {
 88 | 	// First try by extension
 89 | 	ext := filepath.Ext(path)
 90 | 	if ext != "" {
 91 | 		mimeType := mime.TypeByExtension(ext)
 92 | 		if mimeType != "" {
 93 | 			return mimeType
 94 | 		}
 95 | 	}
 96 | 
 97 | 	// If that fails, try to read a bit of the file
 98 | 	file, err := os.Open(path)
 99 | 	if err != nil {
100 | 		return "application/octet-stream" // Default
101 | 	}
102 | 	defer file.Close()
103 | 
104 | 	// Read first 512 bytes to detect content type
105 | 	buffer := make([]byte, 512)
106 | 	n, err := file.Read(buffer)
107 | 	if err != nil {
108 | 		return "application/octet-stream" // Default
109 | 	}
110 | 
111 | 	// Use http.DetectContentType
112 | 	return http.DetectContentType(buffer[:n])
113 | }
114 | 
115 | // IsTextFile determines if a file is likely a text file based on MIME type
116 | func IsTextFile(mimeType string) bool {
117 | 	return strings.HasPrefix(mimeType, "text/") ||
118 | 		mimeType == "application/json" ||
119 | 		mimeType == "application/xml" ||
120 | 		mimeType == "application/javascript" ||
121 | 		mimeType == "application/x-javascript" ||
122 | 		strings.Contains(mimeType, "+xml") ||
123 | 		strings.Contains(mimeType, "+json")
124 | }
125 | 
126 | // IsImageFile determines if a file is an image based on MIME type
127 | func IsImageFile(mimeType string) bool {
128 | 	return strings.HasPrefix(mimeType, "image/")
129 | }
130 | 
131 | // PathToResourceURI converts a file path to a resource URI
132 | func PathToResourceURI(path string) string {
133 | 	return "file://" + path
134 | }
135 | 
```

--------------------------------------------------------------------------------
/pkg/server/server.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //	http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | package server
 17 | 
 18 | import (
 19 | 	"context"
 20 | 	"fmt"
 21 | 	"log"
 22 | 	"os"
 23 | 	"strings"
 24 | 	"time"
 25 | 
 26 | 	"github.com/mark3labs/mcp-go/server"
 27 | 	"github.com/rs/zerolog"
 28 | 
 29 | 	"github.com/gojue/moling/pkg/comm"
 30 | 	"github.com/gojue/moling/pkg/config"
 31 | 	"github.com/gojue/moling/pkg/services/abstract"
 32 | )
 33 | 
 34 | type MoLingServer struct {
 35 | 	ctx        context.Context
 36 | 	server     *server.MCPServer
 37 | 	services   []abstract.Service
 38 | 	logger     zerolog.Logger
 39 | 	mlConfig   config.MoLingConfig
 40 | 	listenAddr string // SSE mode listen address, if empty, use STDIO mode.
 41 | }
 42 | 
 43 | func NewMoLingServer(ctx context.Context, srvs []abstract.Service, mlConfig config.MoLingConfig) (*MoLingServer, error) {
 44 | 	mcpServer := server.NewMCPServer(
 45 | 		mlConfig.ServerName,
 46 | 		mlConfig.Version,
 47 | 		server.WithResourceCapabilities(true, true),
 48 | 		server.WithLogging(),
 49 | 		server.WithPromptCapabilities(true),
 50 | 	)
 51 | 	// Set the context for the server
 52 | 	ms := &MoLingServer{
 53 | 		ctx:        ctx,
 54 | 		server:     mcpServer,
 55 | 		services:   srvs,
 56 | 		listenAddr: mlConfig.ListenAddr,
 57 | 		logger:     ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger),
 58 | 		mlConfig:   mlConfig,
 59 | 	}
 60 | 	err := ms.init()
 61 | 	return ms, err
 62 | }
 63 | 
 64 | func (m *MoLingServer) init() error {
 65 | 	var err error
 66 | 	for _, srv := range m.services {
 67 | 		m.logger.Debug().Str("serviceName", string(srv.Name())).Msg("Loading service")
 68 | 		err = m.loadService(srv)
 69 | 		if err != nil {
 70 | 			m.logger.Info().Err(err).Str("serviceName", string(srv.Name())).Msg("Failed to load service")
 71 | 		}
 72 | 	}
 73 | 	return err
 74 | }
 75 | 
 76 | func (m *MoLingServer) loadService(srv abstract.Service) error {
 77 | 
 78 | 	// Add resources
 79 | 	for r, rhf := range srv.Resources() {
 80 | 		m.server.AddResource(r, rhf)
 81 | 	}
 82 | 
 83 | 	// Add Resource Templates
 84 | 	for rt, rthf := range srv.ResourceTemplates() {
 85 | 		m.server.AddResourceTemplate(rt, rthf)
 86 | 	}
 87 | 
 88 | 	// Add Tools
 89 | 	m.server.AddTools(srv.Tools()...)
 90 | 
 91 | 	// Add Notification Handlers
 92 | 	for n, nhf := range srv.NotificationHandlers() {
 93 | 		m.server.AddNotificationHandler(n, nhf)
 94 | 	}
 95 | 
 96 | 	// Add Prompts
 97 | 	for _, pe := range srv.Prompts() {
 98 | 		// Add Prompt
 99 | 		m.server.AddPrompt(pe.Prompt(), pe.Handler())
100 | 	}
101 | 	return nil
102 | }
103 | 
104 | func (m *MoLingServer) Serve() error {
105 | 	mLogger := log.New(m.logger, m.mlConfig.ServerName, 0)
106 | 	if m.listenAddr != "" {
107 | 		ltnAddr := fmt.Sprintf("http://%s", strings.TrimPrefix(m.listenAddr, "http://"))
108 | 		consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
109 | 		multi := zerolog.MultiLevelWriter(consoleWriter, m.logger)
110 | 		m.logger = zerolog.New(multi).With().Timestamp().Logger()
111 | 		m.logger.Info().Str("listenAddr", m.listenAddr).Str("BaseURL", ltnAddr).Msg("Starting SSE server")
112 | 		m.logger.Warn().Msgf("The SSE server URL must be: %s. Please do not make mistakes, even if it is another IP or domain name on the same computer, it cannot be mixed.", ltnAddr)
113 | 		return server.NewSSEServer(m.server, server.WithBaseURL(ltnAddr)).Start(m.listenAddr)
114 | 	}
115 | 	m.logger.Info().Msg("Starting STDIO server")
116 | 	return server.ServeStdio(m.server, server.WithErrorLogger(mLogger))
117 | }
118 | 
```

--------------------------------------------------------------------------------
/cli/cobrautl/help.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | package cobrautl
 18 | 
 19 | import (
 20 | 	"bytes"
 21 | 	"fmt"
 22 | 	"io"
 23 | 	"os"
 24 | 	"strings"
 25 | 	"text/tabwriter"
 26 | 	"text/template"
 27 | 
 28 | 	"github.com/spf13/cobra"
 29 | 	"github.com/spf13/pflag"
 30 | )
 31 | 
 32 | var (
 33 | 	commandUsageTemplate *template.Template
 34 | 	templFuncs           = template.FuncMap{
 35 | 		"descToLines": func(s string) []string {
 36 | 			// trim leading/trailing whitespace and split into slice of lines
 37 | 			return strings.Split(strings.Trim(s, "\n\t "), "\n")
 38 | 		},
 39 | 		"cmdName": func(cmd *cobra.Command, startCmd *cobra.Command) string {
 40 | 			parts := []string{cmd.Name()}
 41 | 			for cmd.HasParent() && cmd.Parent().Name() != startCmd.Name() {
 42 | 				cmd = cmd.Parent()
 43 | 				parts = append([]string{cmd.Name()}, parts...)
 44 | 			}
 45 | 			return strings.Join(parts, " ")
 46 | 		},
 47 | 	}
 48 | )
 49 | 
 50 | func init() {
 51 | 	commandUsage := `
 52 | {{ $cmd := .Cmd }}\
 53 | {{ $cmdname := cmdName .Cmd .Cmd.Root }}\
 54 | NAME:
 55 | {{ if not .Cmd.HasParent }}\
 56 | {{printf "\t%s - %s" .Cmd.Name .Cmd.Short}}
 57 | {{else}}\
 58 | {{printf "\t%s - %s" $cmdname .Cmd.Short}}
 59 | {{end}}\
 60 | 
 61 | USAGE:
 62 | {{printf "\t%s" .Cmd.UseLine}}
 63 | {{ if not .Cmd.HasParent }}\
 64 | 
 65 | VERSION:
 66 | {{printf "\t%s" .Version}}
 67 | {{end}}\
 68 | {{if .Cmd.HasSubCommands}}\
 69 | 
 70 | COMMANDS:
 71 | {{range .SubCommands}}\
 72 | {{ $cmdname := cmdName . $cmd }}\
 73 | {{ if .Runnable }}\
 74 | {{printf "\t%s\t%s" $cmdname .Short}}
 75 | {{end}}\
 76 | {{end}}\
 77 | {{end}}\
 78 | {{ if .Cmd.Long }}\
 79 | 
 80 | DESCRIPTION:
 81 | {{range $line := descToLines .Cmd.Long}}{{printf "\t%s" $line}}
 82 | {{end}}\
 83 | {{end}}\
 84 | {{if .Cmd.HasLocalFlags}}\
 85 | 
 86 | OPTIONS:
 87 | {{.LocalFlags}}\
 88 | {{end}}\
 89 | {{if .Cmd.HasInheritedFlags}}\
 90 | 
 91 | GLOBAL OPTIONS:
 92 | {{.GlobalFlags}}\
 93 | {{end}}
 94 | `[1:]
 95 | 
 96 | 	commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(strings.Replace(commandUsage, "\\\n", "", -1)))
 97 | }
 98 | 
 99 | func molingFlagUsages(flagSet *pflag.FlagSet) string {
100 | 	x := new(bytes.Buffer)
101 | 
102 | 	flagSet.VisitAll(func(flag *pflag.Flag) {
103 | 		if len(flag.Deprecated) > 0 {
104 | 			return
105 | 		}
106 | 		var format string
107 | 		if len(flag.Shorthand) > 0 {
108 | 			format = "  -%s, --%s"
109 | 		} else {
110 | 			format = "   %s   --%s"
111 | 		}
112 | 		if len(flag.NoOptDefVal) > 0 {
113 | 			format = format + "["
114 | 		}
115 | 		if flag.Value.Type() == "string" {
116 | 			// put quotes on the value
117 | 			format = format + "=%q"
118 | 		} else {
119 | 			format = format + "=%s"
120 | 		}
121 | 		if len(flag.NoOptDefVal) > 0 {
122 | 			format = format + "]"
123 | 		}
124 | 		format = format + "\t%s\n"
125 | 		shorthand := flag.Shorthand
126 | 		fmt.Fprintf(x, format, shorthand, flag.Name, flag.DefValue, flag.Usage)
127 | 	})
128 | 
129 | 	return x.String()
130 | }
131 | 
132 | func getSubCommands(cmd *cobra.Command) []*cobra.Command {
133 | 	var subCommands []*cobra.Command
134 | 	for _, subCmd := range cmd.Commands() {
135 | 		subCommands = append(subCommands, subCmd)
136 | 		subCommands = append(subCommands, getSubCommands(subCmd)...)
137 | 	}
138 | 	return subCommands
139 | }
140 | 
141 | func UsageFunc(cmd *cobra.Command, version string) error {
142 | 	subCommands := getSubCommands(cmd)
143 | 	tabOut := getTabOutWithWriter(os.Stdout)
144 | 	err := commandUsageTemplate.Execute(tabOut, struct {
145 | 		Cmd         *cobra.Command
146 | 		LocalFlags  string
147 | 		GlobalFlags string
148 | 		SubCommands []*cobra.Command
149 | 		Version     string
150 | 	}{
151 | 		cmd,
152 | 		molingFlagUsages(cmd.LocalFlags()),
153 | 		molingFlagUsages(cmd.InheritedFlags()),
154 | 		subCommands,
155 | 		version,
156 | 	})
157 | 	if err != nil {
158 | 		return err
159 | 	}
160 | 	err = tabOut.Flush()
161 | 	return err
162 | }
163 | 
164 | func getTabOutWithWriter(writer io.Writer) *tabwriter.Writer {
165 | 	aTabOut := new(tabwriter.Writer)
166 | 	aTabOut.Init(writer, 0, 8, 1, '\t', 0)
167 | 	return aTabOut
168 | }
169 | 
```

--------------------------------------------------------------------------------
/pkg/services/filesystem/file_system_config.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | package filesystem
 18 | 
 19 | import (
 20 | 	"fmt"
 21 | 	"os"
 22 | 	"path/filepath"
 23 | 	"strings"
 24 | )
 25 | 
 26 | const (
 27 | 	// FileSystemPromptDefault is the default prompt for the file system.
 28 | 	FileSystemPromptDefault = `
 29 | You are a powerful local filesystem management assistant capable of performing various file operations and management tasks. Your capabilities include:
 30 | 
 31 | 1. **File Browsing**: Navigate to specified directories to load lists of files and folders.
 32 | 
 33 | 2. **File Operations**:
 34 |    - Create new files or folders
 35 |    - Delete specified files or folders
 36 |    - Copy and move files and folders
 37 |    - Rename files or folders
 38 | 
 39 | 3. **File Content Operations**:
 40 |    - Read the contents of text files and return them
 41 |    - Write text to specified files
 42 |    - Append content to existing files
 43 | 
 44 | 4. **File Information Retrieval**:
 45 |    - Retrieve properties of files or folders (e.g., size, creation date, modification date)
 46 |    - Check if files or folders exist
 47 | 
 48 | 5. **Search Functionality**:
 49 |    - Search for files in specified directories, supporting wildcard matching
 50 |    - Filter search results by file type or modification date
 51 | 
 52 | For all actions, please provide clear instructions, including:
 53 | - The specific action you want to perform
 54 | - Required parameters (directory paths, filenames, content, etc.)
 55 | - Any optional parameters (e.g., new filenames, search patterns, etc.)
 56 | - Relevant expected outcomes
 57 | 
 58 | You should confirm actions before execution when dealing with sensitive operations or destructive commands. Report back with clear status updates, success/failure indicators, and any relevant output or results.
 59 | `
 60 | )
 61 | 
 62 | var (
 63 | 	allowedDirsDefault = os.TempDir()
 64 | )
 65 | 
 66 | // FileSystemConfig represents the configuration for the file system.
 67 | type FileSystemConfig struct {
 68 | 	PromptFile  string `json:"prompt_file"` // PromptFile is the prompt file for the file system.
 69 | 	prompt      string
 70 | 	AllowedDir  string `json:"allowed_dir"` // AllowedDirs is a list of allowed directories. split by comma. e.g. /tmp,/var/tmp
 71 | 	allowedDirs []string
 72 | 	CachePath   string `json:"cache_path"` // CachePath is the root path for the file system.
 73 | }
 74 | 
 75 | // NewFileSystemConfig creates a new FileSystemConfig with the given allowed directories.
 76 | func NewFileSystemConfig(path string) *FileSystemConfig {
 77 | 	paths := strings.Split(path, ",")
 78 | 	path = ""
 79 | 	if strings.TrimSpace(path) == "" {
 80 | 		path = allowedDirsDefault
 81 | 		paths = []string{allowedDirsDefault}
 82 | 	}
 83 | 
 84 | 	return &FileSystemConfig{
 85 | 		AllowedDir:  path,
 86 | 		CachePath:   path,
 87 | 		allowedDirs: paths,
 88 | 	}
 89 | }
 90 | 
 91 | // Check validates the allowed directories in the FileSystemConfig.
 92 | func (fc *FileSystemConfig) Check() error {
 93 | 	fc.prompt = FileSystemPromptDefault
 94 | 	normalized := make([]string, 0, len(fc.allowedDirs))
 95 | 	for _, dir := range fc.allowedDirs {
 96 | 		abs, err := filepath.Abs(strings.TrimSpace(dir))
 97 | 		if err != nil {
 98 | 			return fmt.Errorf("failed to resolve path %s: %w", dir, err)
 99 | 		}
100 | 		info, err := os.Stat(abs)
101 | 		if err != nil {
102 | 			return fmt.Errorf("failed to access directory %s: %w", abs, err)
103 | 		}
104 | 		if !info.IsDir() {
105 | 			return fmt.Errorf("path is not a directory: %s", abs)
106 | 		}
107 | 
108 | 		normalized = append(normalized, filepath.Clean(abs)+string(filepath.Separator))
109 | 	}
110 | 	fc.allowedDirs = normalized
111 | 
112 | 	if fc.PromptFile != "" {
113 | 		read, err := os.ReadFile(fc.PromptFile)
114 | 		if err != nil {
115 | 			return fmt.Errorf("failed to read prompt file:%s, error: %w", fc.PromptFile, err)
116 | 		}
117 | 		fc.prompt = string(read)
118 | 	}
119 | 
120 | 	return nil
121 | }
122 | 
```

--------------------------------------------------------------------------------
/pkg/services/command/command_config.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | package command
 18 | 
 19 | import (
 20 | 	"fmt"
 21 | 	"os"
 22 | 	"strings"
 23 | )
 24 | 
 25 | const (
 26 | 	CommandPromptDefault = `
 27 | You are a powerful terminal command assistant capable of executing various command-line on %s operations and management tasks. Your capabilities include:
 28 | 
 29 | 1. **File and Directory Management**:
 30 |     - List files and subdirectories in a directory
 31 |     - Create new files or directories
 32 |     - Delete specified files or directories
 33 |     - Copy and move files and directories
 34 |     - Rename files or directories
 35 | 
 36 | 2. **File Content Operations**:
 37 |     - View the contents of text files
 38 |     - Edit file contents
 39 |     - Redirect output to a file
 40 |     - Search file contents
 41 | 
 42 | 3. **System Information Retrieval**:
 43 |     - Retrieve system information (e.g., CPU usage, memory usage, etc.)
 44 |     - View the current user and their permissions
 45 |     - Check the current working directory
 46 | 
 47 | 4. **Network Operations**:
 48 |     - Check network connection status (e.g., using the ping command)
 49 |     - Query domain information (e.g., using the whois command)
 50 |     - Manage network services (e.g., start, stop, and restart services)
 51 | 
 52 | 5. **Process Management**:
 53 |     - List currently running processes
 54 |     - Terminate specified processes
 55 |     - Adjust process priorities
 56 | 
 57 | Before executing any actions, please provide clear instructions, including:
 58 | - The specific command you want to execute
 59 | - Required parameters (file paths, directory names, etc.)
 60 | - Any optional parameters (e.g., modification options, output formats, etc.)
 61 | - Relevant expected results or output
 62 | 
 63 | When dealing with sensitive operations or destructive commands, please confirm before execution. Report back with clear status updates, success/failure indicators, and any relevant output or results.
 64 | `
 65 | )
 66 | 
 67 | // CommandConfig represents the configuration for allowed commands.
 68 | type CommandConfig struct {
 69 | 	PromptFile      string `json:"prompt_file"` // PromptFile is the prompt file for the command.
 70 | 	prompt          string
 71 | 	AllowedCommand  string `json:"allowed_command"` // AllowedCommand is a list of allowed command. split by comma. e.g. ls,cat,echo
 72 | 	allowedCommands []string
 73 | }
 74 | 
 75 | var (
 76 | 	allowedCmdDefault = []string{
 77 | 		"ls", "cat", "echo", "pwd", "head", "tail", "grep", "find", "stat", "df",
 78 | 		"du", "free", "top", "ps", "uptime", "who", "w", "last", "uname", "hostname",
 79 | 		"ifconfig", "netstat", "ping", "traceroute", "route", "ip", "ss", "lsof", "vmstat",
 80 | 		"iostat", "mpstat", "sar", "uptime", "cut", "sort", "uniq", "wc", "awk", "sed",
 81 | 		"diff", "cmp", "comm", "file", "basename", "dirname", "chmod", "chown", "curl",
 82 | 		"nslookup", "dig", "host", "ssh", "scp", "sftp", "ftp", "wget", "tar", "gzip",
 83 | 		"scutil", "networksetup, git", "cd",
 84 | 	}
 85 | )
 86 | 
 87 | // NewCommandConfig creates a new CommandConfig with the given allowed commands.
 88 | func NewCommandConfig() *CommandConfig {
 89 | 	return &CommandConfig{
 90 | 		allowedCommands: allowedCmdDefault,
 91 | 		AllowedCommand:  strings.Join(allowedCmdDefault, ","),
 92 | 	}
 93 | }
 94 | 
 95 | // Check validates the allowed commands in the CommandConfig.
 96 | func (cc *CommandConfig) Check() error {
 97 | 	cc.prompt = CommandPromptDefault
 98 | 	var cnt int
 99 | 	cnt = len(cc.allowedCommands)
100 | 
101 | 	// Check if any command is empty
102 | 	for _, cmd := range cc.allowedCommands {
103 | 		if cmd == "" {
104 | 			cnt -= 1
105 | 		}
106 | 	}
107 | 
108 | 	if cnt <= 0 {
109 | 		return fmt.Errorf("no allowed commands specified")
110 | 	}
111 | 	if cc.PromptFile != "" {
112 | 		read, err := os.ReadFile(cc.PromptFile)
113 | 		if err != nil {
114 | 			return fmt.Errorf("failed to read prompt file:%s, error: %w", cc.PromptFile, err)
115 | 		}
116 | 		cc.prompt = string(read)
117 | 	}
118 | 	return nil
119 | }
120 | 
```

--------------------------------------------------------------------------------
/cli/cmd/config.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | package cmd
 18 | 
 19 | import (
 20 | 	"bytes"
 21 | 	"context"
 22 | 	"encoding/json"
 23 | 	"fmt"
 24 | 	"os"
 25 | 	"path/filepath"
 26 | 	"time"
 27 | 
 28 | 	"github.com/rs/zerolog"
 29 | 	"github.com/spf13/cobra"
 30 | 
 31 | 	"github.com/gojue/moling/pkg/comm"
 32 | 	"github.com/gojue/moling/pkg/services"
 33 | )
 34 | 
 35 | var configCmd = &cobra.Command{
 36 | 	Use:   "config",
 37 | 	Short: "Show the configuration of the current service list",
 38 | 	Long: `Show the configuration of the current service list. You can refer to the configuration file to modify the configuration.
 39 | `,
 40 | 	RunE: ConfigCommandFunc,
 41 | }
 42 | 
 43 | var (
 44 | 	initial bool
 45 | )
 46 | 
 47 | // ConfigCommandFunc executes the "config" command.
 48 | func ConfigCommandFunc(command *cobra.Command, args []string) error {
 49 | 	var err error
 50 | 	logger := initLogger(mlConfig.BasePath)
 51 | 	consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
 52 | 	multi := zerolog.MultiLevelWriter(consoleWriter, logger)
 53 | 	logger = zerolog.New(multi).With().Timestamp().Logger()
 54 | 	mlConfig.SetLogger(logger)
 55 | 	logger.Info().Msg("Start to show config")
 56 | 	ctx := context.WithValue(context.Background(), comm.MoLingConfigKey, mlConfig)
 57 | 	ctx = context.WithValue(ctx, comm.MoLingLoggerKey, logger)
 58 | 
 59 | 	// 当前配置文件检测
 60 | 	hasConfig := false
 61 | 	var nowConfig []byte
 62 | 	nowConfigJSON := make(map[string]any)
 63 | 	configFilePath := filepath.Join(mlConfig.BasePath, mlConfig.ConfigFile)
 64 | 	if nowConfig, err = os.ReadFile(configFilePath); err == nil {
 65 | 		hasConfig = true
 66 | 	}
 67 | 	if hasConfig {
 68 | 		err = json.Unmarshal(nowConfig, &nowConfigJSON)
 69 | 		if err != nil {
 70 | 			return fmt.Errorf("error unmarshaling JSON: %w, payload:%s", err, string(nowConfig))
 71 | 		}
 72 | 	}
 73 | 
 74 | 	bf := bytes.Buffer{}
 75 | 	bf.WriteString("\n{\n")
 76 | 
 77 | 	// 写入GlobalConfig
 78 | 	mlConfigJSON, err := json.Marshal(mlConfig)
 79 | 	if err != nil {
 80 | 		return fmt.Errorf("error marshaling GlobalConfig: %w", err)
 81 | 	}
 82 | 	bf.WriteString("\t\"MoLingConfig\":\n")
 83 | 	bf.WriteString(fmt.Sprintf("\t%s,\n", mlConfigJSON))
 84 | 	first := true
 85 | 	for srvName, nsv := range services.ServiceList() {
 86 | 		// 获取服务对应的配置
 87 | 		cfg, ok := nowConfigJSON[string(srvName)].(map[string]any)
 88 | 
 89 | 		srv, err := nsv(ctx)
 90 | 		if err != nil {
 91 | 			return err
 92 | 		}
 93 | 		// srv Loadconfig
 94 | 		if ok {
 95 | 			err = srv.LoadConfig(cfg)
 96 | 			if err != nil {
 97 | 				return fmt.Errorf("error loading config for service %s: %w", srv.Name(), err)
 98 | 			}
 99 | 		} else {
100 | 			logger.Debug().Str("service", string(srv.Name())).Msg("Service not found in config, using default config")
101 | 		}
102 | 		// srv Init
103 | 		err = srv.Init()
104 | 		if err != nil {
105 | 			return fmt.Errorf("error initializing service %s: %w", srv.Name(), err)
106 | 		}
107 | 		if !first {
108 | 			bf.WriteString(",\n")
109 | 		}
110 | 		bf.WriteString(fmt.Sprintf("\t\"%s\":\n", srv.Name()))
111 | 		bf.WriteString(fmt.Sprintf("\t%s\n", srv.Config()))
112 | 		first = false
113 | 	}
114 | 	bf.WriteString("}\n")
115 | 	// 解析原始 JSON 字符串
116 | 	var data any
117 | 	err = json.Unmarshal(bf.Bytes(), &data)
118 | 	if err != nil {
119 | 		return fmt.Errorf("error unmarshaling JSON: %w, payload:%s", err, bf.String())
120 | 	}
121 | 
122 | 	// 格式化 JSON
123 | 	formattedJSON, err := json.MarshalIndent(data, "", "  ")
124 | 	if err != nil {
125 | 		return fmt.Errorf("error marshaling JSON: %w", err)
126 | 	}
127 | 
128 | 	// 如果不存在配置文件
129 | 	if !hasConfig {
130 | 		logger.Info().Msgf("Configuration file %s does not exist. Creating a new one.", configFilePath)
131 | 		err = os.WriteFile(configFilePath, formattedJSON, 0644)
132 | 		if err != nil {
133 | 			return fmt.Errorf("error writing configuration file: %w", err)
134 | 		}
135 | 		logger.Info().Msgf("Configuration file %s created successfully.", configFilePath)
136 | 	}
137 | 	logger.Info().Str("config", configFilePath).Msg("Current loaded configuration file path")
138 | 	logger.Info().Msg("You can modify the configuration file to change the settings.")
139 | 	if !initial {
140 | 		logger.Info().Msgf("Configuration details: \n%s\n", formattedJSON)
141 | 	}
142 | 	return nil
143 | }
144 | 
145 | func init() {
146 | 	configCmd.PersistentFlags().BoolVar(&initial, "init", false, fmt.Sprintf("Save configuration to %s", filepath.Join(mlConfig.BasePath, mlConfig.ConfigFile)))
147 | 	rootCmd.AddCommand(configCmd)
148 | }
149 | 
```

--------------------------------------------------------------------------------
/pkg/services/browser/browser_config.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | // Package services provides a set of services for the MoLing application.
 18 | package browser
 19 | 
 20 | import (
 21 | 	"fmt"
 22 | 	"os"
 23 | 	"path/filepath"
 24 | )
 25 | 
 26 | const BrowserPromptDefault = `
 27 | You are an AI-powered browser automation assistant capable of performing a wide range of web interactions and debugging tasks. Your capabilities include:
 28 | 
 29 | 1. **Navigation**: Navigate to any specified URL to load web pages.
 30 | 
 31 | 2. **Screenshot Capture**: Take full-page screenshots or capture specific elements using CSS selectors, with customizable dimensions (default: 1700x1100 pixels).
 32 | 
 33 | 3. **Element Interaction**:
 34 |    - Click on elements identified by CSS selectors
 35 |    - Hover over specified elements
 36 |    - Fill input fields with provided values
 37 |    - Select options in dropdown menus
 38 | 
 39 | 4. **JavaScript Execution**:
 40 |    - Run arbitrary JavaScript code in the browser context
 41 |    - Evaluate scripts and return results
 42 | 
 43 | 5. **Debugging Tools**:
 44 |    - Enable/disable JavaScript debugging mode
 45 |    - Set breakpoints at specific script locations (URL + line number + optional column/condition)
 46 |    - Remove existing breakpoints by ID
 47 |    - Pause and resume script execution
 48 |    - Retrieve current call stack when paused
 49 | 
 50 | For all actions requiring element selection, you must use precise CSS selectors. When capturing screenshots, you can specify either the entire page or target specific elements. For debugging operations, you can precisely control execution flow and inspect runtime behavior.
 51 | 
 52 | Please provide clear instructions including:
 53 | - The specific action you want performed
 54 | - Required parameters (URLs, selectors, values, etc.)
 55 | - Any optional parameters (dimensions, conditions, etc.)
 56 | - Expected outcomes where relevant
 57 | 
 58 | You should confirm actions before execution when dealing with sensitive operations or destructive commands. Report back with clear status updates, success/failure indicators, and any relevant output or captured data.
 59 | `
 60 | 
 61 | type BrowserConfig struct {
 62 | 	PromptFile           string `json:"prompt_file"` // PromptFile is the prompt file for the browser.
 63 | 	prompt               string
 64 | 	Headless             bool   `json:"headless"`
 65 | 	Timeout              int    `json:"timeout"`
 66 | 	Proxy                string `json:"proxy"`
 67 | 	UserAgent            string `json:"user_agent"`
 68 | 	DefaultLanguage      string `json:"default_language"`
 69 | 	URLTimeout           int    `json:"url_timeout"`            // URLTimeout is the timeout for loading a URL. time.Second
 70 | 	SelectorQueryTimeout int    `json:"selector_query_timeout"` // SelectorQueryTimeout is the timeout for CSS selector queries. time.Second
 71 | 	DataPath             string `json:"data_path"`              // DataPath is the path to the data directory.
 72 | 	BrowserDataPath      string `json:"browser_data_path"`      // BrowserDataPath is the path to the browser data directory.
 73 | }
 74 | 
 75 | func (cfg *BrowserConfig) Check() error {
 76 | 	cfg.prompt = BrowserPromptDefault
 77 | 	if cfg.Timeout <= 0 {
 78 | 		return fmt.Errorf("timeout must be greater than 0")
 79 | 	}
 80 | 	if cfg.URLTimeout <= 0 {
 81 | 		return fmt.Errorf("URL timeout must be greater than 0")
 82 | 	}
 83 | 	if cfg.SelectorQueryTimeout <= 0 {
 84 | 		return fmt.Errorf("selector Query timeout must be greater than 0")
 85 | 	}
 86 | 	if cfg.PromptFile != "" {
 87 | 		read, err := os.ReadFile(cfg.PromptFile)
 88 | 		if err != nil {
 89 | 			return fmt.Errorf("failed to read prompt file:%s, error: %w", cfg.PromptFile, err)
 90 | 		}
 91 | 		cfg.prompt = string(read)
 92 | 	}
 93 | 	return nil
 94 | }
 95 | 
 96 | // NewBrowserConfig creates a new BrowserConfig with default values.
 97 | func NewBrowserConfig() *BrowserConfig {
 98 | 	return &BrowserConfig{
 99 | 		Headless:             false,
100 | 		Timeout:              30,
101 | 		URLTimeout:           10,
102 | 		SelectorQueryTimeout: 10,
103 | 		UserAgent:            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
104 | 		DefaultLanguage:      "en-US",
105 | 		DataPath:             filepath.Join(os.TempDir(), ".moling", "data"),
106 | 	}
107 | }
108 | 
```

--------------------------------------------------------------------------------
/pkg/services/abstract/mlservice_test.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | package abstract
 18 | 
 19 | import (
 20 | 	"context"
 21 | 	"testing"
 22 | 
 23 | 	"github.com/mark3labs/mcp-go/mcp"
 24 | )
 25 | 
 26 | func TestMLService_AddResource(t *testing.T) {
 27 | 	service := &MLService{}
 28 | 	err := service.InitResources()
 29 | 	if err != nil {
 30 | 		t.Fatalf("Failed to initialize MLService: %s", err.Error())
 31 | 	}
 32 | 	resource := mcp.Resource{Name: "testResource"}
 33 | 	handler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
 34 | 		return []mcp.ResourceContents{
 35 | 			mcp.TextResourceContents{
 36 | 				Text:     "text",
 37 | 				URI:      "uri",
 38 | 				MIMEType: "text/plain",
 39 | 			},
 40 | 		}, nil
 41 | 	}
 42 | 
 43 | 	service.AddResource(resource, handler)
 44 | 
 45 | 	if len(service.resources) != 1 {
 46 | 		t.Errorf("Expected 1 resource, got %d", len(service.resources))
 47 | 	}
 48 | 	if service.resources[resource] == nil {
 49 | 		t.Errorf("Handler for resource not found")
 50 | 	}
 51 | }
 52 | 
 53 | func TestMLService_AddResourceTemplate(t *testing.T) {
 54 | 	service := &MLService{}
 55 | 	err := service.InitResources()
 56 | 	if err != nil {
 57 | 		t.Fatalf("Failed to initialize MLService: %s", err.Error())
 58 | 	}
 59 | 	template := mcp.ResourceTemplate{Name: "testTemplate"}
 60 | 	handler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
 61 | 		return []mcp.ResourceContents{
 62 | 			mcp.TextResourceContents{
 63 | 				Text:     "text",
 64 | 				URI:      "uri",
 65 | 				MIMEType: "text/plain",
 66 | 			},
 67 | 		}, nil
 68 | 	}
 69 | 
 70 | 	service.AddResourceTemplate(template, handler)
 71 | 
 72 | 	if len(service.resourcesTemplates) != 1 {
 73 | 		t.Errorf("Expected 1 resource template, got %d", len(service.resourcesTemplates))
 74 | 	}
 75 | 	if service.resourcesTemplates[template] == nil {
 76 | 		t.Errorf("Handler for resource template not found")
 77 | 	}
 78 | }
 79 | 
 80 | func TestMLService_AddPrompt(t *testing.T) {
 81 | 	service := &MLService{}
 82 | 	err := service.InitResources()
 83 | 	if err != nil {
 84 | 		t.Fatalf("Failed to initialize MLService: %s", err.Error())
 85 | 	}
 86 | 	prompt := "testPrompt"
 87 | 	handler := func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 88 | 		pms := make([]mcp.PromptMessage, 0)
 89 | 		pms = append(pms, mcp.PromptMessage{
 90 | 			Role: mcp.RoleUser,
 91 | 			Content: mcp.TextContent{
 92 | 				Type: "text",
 93 | 				Text: "Prompt response",
 94 | 			},
 95 | 		})
 96 | 		return &mcp.GetPromptResult{
 97 | 			Description: "prompt description",
 98 | 			Messages:    pms,
 99 | 		}, nil
100 | 	}
101 | 	pe := PromptEntry{
102 | 		PromptVar:   mcp.Prompt{Name: "testPrompt"},
103 | 		HandlerFunc: handler,
104 | 	}
105 | 	service.AddPrompt(pe)
106 | 
107 | 	if len(service.prompts) != 1 {
108 | 		t.Errorf("Expected 1 prompt, got %d", len(service.prompts))
109 | 	}
110 | 	for _, p := range service.prompts {
111 | 		if p.PromptVar.Name != prompt {
112 | 			t.Errorf("Expected prompt name %s, got %s", prompt, p.PromptVar.Name)
113 | 		}
114 | 	}
115 | }
116 | 
117 | func TestMLService_AddTool(t *testing.T) {
118 | 	service := &MLService{}
119 | 	err := service.InitResources()
120 | 	if err != nil {
121 | 		t.Fatalf("Failed to initialize MLService: %s", err.Error())
122 | 	}
123 | 	tool := mcp.Tool{Name: "testTool"}
124 | 	handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
125 | 		return &mcp.CallToolResult{
126 | 			Content: []mcp.Content{
127 | 				mcp.TextContent{
128 | 					Type: "text",
129 | 					Text: "Prompt response",
130 | 				},
131 | 			},
132 | 		}, nil
133 | 	}
134 | 
135 | 	service.AddTool(tool, handler)
136 | 
137 | 	if len(service.tools) != 1 {
138 | 		t.Errorf("Expected 1 tool, got %d", len(service.tools))
139 | 	}
140 | 
141 | 	// After
142 | 	if service.tools[0].Tool.Name != tool.Name {
143 | 		t.Errorf("Tool not added correctly")
144 | 	}
145 | }
146 | 
147 | func TestMLService_AddNotificationHandler(t *testing.T) {
148 | 	service := &MLService{}
149 | 	err := service.InitResources()
150 | 	if err != nil {
151 | 		t.Fatalf("Failed to initialize MLService: %s", err.Error())
152 | 	}
153 | 	name := "testHandler"
154 | 	handler := func(ctx context.Context, n mcp.JSONRPCNotification) {
155 | 		t.Logf("Received notification: %s", n.Method)
156 | 	}
157 | 
158 | 	service.AddNotificationHandler(name, handler)
159 | 
160 | 	if len(service.notificationHandlers) != 1 {
161 | 		t.Errorf("Expected 1 notification handler, got %d", len(service.notificationHandlers))
162 | 	}
163 | 	if service.notificationHandlers[name] == nil {
164 | 		t.Errorf("Handler for notification not found")
165 | 	}
166 | }
167 | 
```

--------------------------------------------------------------------------------
/client/client.go:
--------------------------------------------------------------------------------

```go
  1 | /*
  2 |  *
  3 |  *  Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  4 |  *
  5 |  *  Licensed under the Apache License, Version 2.0 (the "License");
  6 |  *  you may not use this file except in compliance with the License.
  7 |  *  You may obtain a copy of the License at
  8 |  *
  9 |  *    http://www.apache.org/licenses/LICENSE-2.0
 10 |  *
 11 |  *  Unless required by applicable law or agreed to in writing, software
 12 |  *  distributed under the License is distributed on an "AS IS" BASIS,
 13 |  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14 |  *  See the License for the specific language governing permissions and
 15 |  *  limitations under the License.
 16 |  *
 17 |  *  Repository: https://github.com/gojue/moling
 18 |  *
 19 |  */
 20 | 
 21 | package client
 22 | 
 23 | import (
 24 | 	"encoding/json"
 25 | 	"errors"
 26 | 	"os"
 27 | 
 28 | 	"github.com/rs/zerolog"
 29 | )
 30 | 
 31 | var (
 32 | 	// ClineConfigPath is the path to the Cline config file.
 33 | 	clientLists = make(map[string]string, 3)
 34 | )
 35 | 
 36 | const MCPServersKey = "mcpServers"
 37 | 
 38 | // MCPServerConfig represents the configuration for the MCP Client.
 39 | type MCPServerConfig struct {
 40 | 	Description string   `json:"description"`       // Description of the MCP Server
 41 | 	IsActive    bool     `json:"isActive"`          // Is the MCP Server active
 42 | 	Command     string   `json:"command,omitempty"` // Command to start the MCP Server, STDIO mode only
 43 | 	Args        []string `json:"args,omitempty"`    // Arguments to pass to the command, STDIO mode only
 44 | 	BaseURL     string   `json:"baseUrl,omitempty"` // Base URL of the MCP Server, SSE mode only
 45 | 	TimeOut     uint16   `json:"timeout,omitempty"` // Timeout for the MCP Server, default is 300 seconds
 46 | 	ServerName  string
 47 | }
 48 | 
 49 | // NewMCPServerConfig creates a new MCPServerConfig instance.
 50 | func NewMCPServerConfig(description string, command string, srvName string) MCPServerConfig {
 51 | 	return MCPServerConfig{
 52 | 		Description: description,
 53 | 		IsActive:    true,
 54 | 		Command:     command,
 55 | 		Args:        []string{"-m", "Browser"},
 56 | 		BaseURL:     "",
 57 | 		ServerName:  srvName,
 58 | 		TimeOut:     300,
 59 | 	}
 60 | }
 61 | 
 62 | // Manager manages the configuration of different clients.
 63 | type Manager struct {
 64 | 	logger    zerolog.Logger
 65 | 	clients   map[string]string
 66 | 	mcpConfig MCPServerConfig
 67 | }
 68 | 
 69 | // NewManager creates a new ClientManager instance.
 70 | func NewManager(lger zerolog.Logger, mcpConfig MCPServerConfig) (cm *Manager) {
 71 | 	cm = &Manager{
 72 | 		clients:   make(map[string]string, 3),
 73 | 		logger:    lger,
 74 | 		mcpConfig: mcpConfig,
 75 | 	}
 76 | 	cm.clients = clientLists
 77 | 	return cm
 78 | }
 79 | 
 80 | // ListClient lists all the clients and checks if they exist.
 81 | func (c *Manager) ListClient() {
 82 | 	for name, path := range c.clients {
 83 | 		c.logger.Debug().Msgf("Client %s: %s", name, path)
 84 | 		if !c.checkExist(path) {
 85 | 			// path not exists
 86 | 			c.logger.Info().Str("Client Name", name).Bool("exist", false).Msg("Client is not exist")
 87 | 		} else {
 88 | 			c.logger.Info().Str("Client Name", name).Bool("exist", true).Msg("Client is exist")
 89 | 		}
 90 | 	}
 91 | 	return
 92 | }
 93 | 
 94 | // SetupConfig sets up the configuration for the clients.
 95 | func (c *Manager) SetupConfig() {
 96 | 	for name, path := range c.clients {
 97 | 		c.logger.Debug().Msgf("Client %s: %s", name, path)
 98 | 		if !c.checkExist(path) {
 99 | 			continue
100 | 		}
101 | 		// read config file
102 | 		file, err := os.ReadFile(path)
103 | 		if err != nil {
104 | 			c.logger.Error().Str("Client Name", name).Msgf("Failed to open config file %s: %s", path, err)
105 | 			continue
106 | 		}
107 | 		c.logger.Debug().Str("Client Name", name).Str("config", string(file)).Send()
108 | 		b, err := c.appendConfig(c.mcpConfig.ServerName, file)
109 | 		if err != nil {
110 | 			c.logger.Error().Str("Client Name", name).Msgf("Failed to append config file %s: %s", path, err)
111 | 			continue
112 | 		}
113 | 		c.logger.Debug().Str("Client Name", name).Str("newConfig", string(b)).Send()
114 | 		// write config file
115 | 		err = os.WriteFile(path, b, 0644)
116 | 		if err != nil {
117 | 			c.logger.Error().Str("Client Name", name).Msgf("Failed to write config file %s: %s", path, err)
118 | 			continue
119 | 		}
120 | 		c.logger.Info().Str("Client Name", name).Msgf("Successfully added config to %s", path)
121 | 	}
122 | }
123 | 
124 | // appendConfig appends the mlMCPConfig to the client config.
125 | func (c *Manager) appendConfig(name string, payload []byte) ([]byte, error) {
126 | 	var err error
127 | 	var jsonMap map[string]any
128 | 	var jsonBytes []byte
129 | 	err = json.Unmarshal(payload, &jsonMap)
130 | 	if err != nil {
131 | 		return nil, err
132 | 	}
133 | 	jsonMcpServer, ok := jsonMap[MCPServersKey].(map[string]any)
134 | 	if !ok {
135 | 		return nil, errors.New("MCPServersKey not found in JSON")
136 | 	}
137 | 	jsonMcpServer[name] = c.mcpConfig
138 | 	jsonMap[MCPServersKey] = jsonMcpServer
139 | 	jsonBytes, err = json.MarshalIndent(jsonMap, "", "  ")
140 | 	if err != nil {
141 | 		return nil, err
142 | 	}
143 | 	return jsonBytes, nil
144 | }
145 | 
146 | // checkExist checks if the file at the given path exists.
147 | func (c *Manager) checkExist(path string) bool {
148 | 	_, err := os.Stat(path)
149 | 	if err != nil {
150 | 		if os.IsNotExist(err) {
151 | 			c.logger.Debug().Msgf("Client config file %s does not exist", path)
152 | 			return false
153 | 		}
154 | 		c.logger.Info().Msgf("check file failed, error:%s", err.Error())
155 | 		return false
156 | 	}
157 | 	return true
158 | }
159 | 
```

--------------------------------------------------------------------------------
/pkg/services/abstract/mlservice.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | package abstract
 18 | 
 19 | import (
 20 | 	"context"
 21 | 	"sync"
 22 | 
 23 | 	"github.com/mark3labs/mcp-go/mcp"
 24 | 	"github.com/mark3labs/mcp-go/server"
 25 | 	"github.com/rs/zerolog"
 26 | 
 27 | 	"github.com/gojue/moling/pkg/config"
 28 | 	"github.com/gojue/moling/pkg/utils"
 29 | )
 30 | 
 31 | type PromptEntry struct {
 32 | 	PromptVar   mcp.Prompt
 33 | 	HandlerFunc server.PromptHandlerFunc
 34 | }
 35 | 
 36 | func (pe *PromptEntry) Prompt() mcp.Prompt {
 37 | 	return pe.PromptVar
 38 | }
 39 | 
 40 | func (pe *PromptEntry) Handler() server.PromptHandlerFunc {
 41 | 	return pe.HandlerFunc
 42 | }
 43 | 
 44 | // NewMLService creates a new MLService with the given context and logger.
 45 | func NewMLService(ctx context.Context, logger zerolog.Logger, cfg *config.MoLingConfig) MLService {
 46 | 	return MLService{
 47 | 		Context:  ctx,
 48 | 		Logger:   logger,
 49 | 		mlConfig: cfg,
 50 | 	}
 51 | }
 52 | 
 53 | // MLService implements the Service interface and provides methods to manage resources, templates, prompts, tools, and notification handlers.
 54 | type MLService struct {
 55 | 	Context context.Context
 56 | 	Logger  zerolog.Logger // The logger for the service
 57 | 
 58 | 	lock                 *sync.Mutex
 59 | 	resources            map[mcp.Resource]server.ResourceHandlerFunc
 60 | 	resourcesTemplates   map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc
 61 | 	prompts              []PromptEntry
 62 | 	tools                []server.ServerTool
 63 | 	notificationHandlers map[string]server.NotificationHandlerFunc
 64 | 	mlConfig             *config.MoLingConfig // The configuration for the service
 65 | }
 66 | 
 67 | // InitResources initializes the MLService with empty maps and a mutex.
 68 | func (mls *MLService) InitResources() error {
 69 | 	mls.lock = &sync.Mutex{}
 70 | 	mls.resources = make(map[mcp.Resource]server.ResourceHandlerFunc)
 71 | 	mls.resourcesTemplates = make(map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc)
 72 | 	mls.prompts = make([]PromptEntry, 0)
 73 | 	mls.notificationHandlers = make(map[string]server.NotificationHandlerFunc)
 74 | 	mls.tools = []server.ServerTool{}
 75 | 	return nil
 76 | }
 77 | 
 78 | // Ctx returns the context of the MLService.
 79 | func (mls *MLService) Ctx() context.Context {
 80 | 	return mls.Context
 81 | }
 82 | 
 83 | // AddResource adds a resource and its handler function to the service.
 84 | func (mls *MLService) AddResource(rs mcp.Resource, hr server.ResourceHandlerFunc) {
 85 | 	mls.lock.Lock()
 86 | 	defer mls.lock.Unlock()
 87 | 	mls.resources[rs] = hr
 88 | }
 89 | 
 90 | // AddResourceTemplate adds a resource template and its handler function to the service.
 91 | func (mls *MLService) AddResourceTemplate(rt mcp.ResourceTemplate, hr server.ResourceTemplateHandlerFunc) {
 92 | 	mls.lock.Lock()
 93 | 	defer mls.lock.Unlock()
 94 | 	mls.resourcesTemplates[rt] = hr
 95 | }
 96 | 
 97 | // AddPrompt adds a prompt and its handler function to the service.
 98 | func (mls *MLService) AddPrompt(pe PromptEntry) {
 99 | 	mls.lock.Lock()
100 | 	defer mls.lock.Unlock()
101 | 	mls.prompts = append(mls.prompts, pe)
102 | }
103 | 
104 | // AddTool adds a tool and its handler function to the service.
105 | func (mls *MLService) AddTool(tool mcp.Tool, handler server.ToolHandlerFunc) {
106 | 	mls.lock.Lock()
107 | 	defer mls.lock.Unlock()
108 | 	mls.tools = append(mls.tools, server.ServerTool{Tool: tool, Handler: handler})
109 | }
110 | 
111 | // AddNotificationHandler adds a notification handler to the service.
112 | func (mls *MLService) AddNotificationHandler(name string, handler server.NotificationHandlerFunc) {
113 | 	mls.lock.Lock()
114 | 	defer mls.lock.Unlock()
115 | 	mls.notificationHandlers[name] = handler
116 | }
117 | 
118 | // Resources returns the map of resources and their handler functions.
119 | func (mls *MLService) Resources() map[mcp.Resource]server.ResourceHandlerFunc {
120 | 	mls.lock.Lock()
121 | 	defer mls.lock.Unlock()
122 | 	return mls.resources
123 | }
124 | 
125 | // ResourceTemplates returns the map of resource templates and their handler functions.
126 | func (mls *MLService) ResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc {
127 | 	mls.lock.Lock()
128 | 	defer mls.lock.Unlock()
129 | 	return mls.resourcesTemplates
130 | }
131 | 
132 | // Prompts returns the map of prompts and their handler functions.
133 | func (mls *MLService) Prompts() []PromptEntry {
134 | 	mls.lock.Lock()
135 | 	defer mls.lock.Unlock()
136 | 	return mls.prompts
137 | }
138 | 
139 | // Tools returns the slice of server tools.
140 | func (mls *MLService) Tools() []server.ServerTool {
141 | 	mls.lock.Lock()
142 | 	defer mls.lock.Unlock()
143 | 	return mls.tools
144 | }
145 | 
146 | // NotificationHandlers returns the map of notification handlers.
147 | func (mls *MLService) NotificationHandlers() map[string]server.NotificationHandlerFunc {
148 | 	mls.lock.Lock()
149 | 	defer mls.lock.Unlock()
150 | 	return mls.notificationHandlers
151 | }
152 | 
153 | // MlConfig returns the configuration of the MoLing service.
154 | func (mls *MLService) MlConfig() *config.MoLingConfig {
155 | 	return mls.mlConfig
156 | }
157 | 
158 | // Config returns the configuration of the service as a string.
159 | func (mls *MLService) Config() string {
160 | 	panic("not implemented yet") // TODO: Implement
161 | }
162 | 
163 | // Name returns the name of the service.
164 | func (mls *MLService) Name() string {
165 | 	panic("not implemented yet") // TODO: Implement
166 | }
167 | 
168 | // LoadConfig loads the configuration for the service from a map.
169 | func (mls *MLService) LoadConfig(jsonData map[string]any) error {
170 | 	//panic("not implemented yet") // TODO: Implement
171 | 	err := utils.MergeJSONToStruct(mls.mlConfig, jsonData)
172 | 	if err != nil {
173 | 		return err
174 | 	}
175 | 	return mls.mlConfig.Check()
176 | }
177 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
  1 | # v0.4.0 (2025-05-31)
  2 | 
  3 | ## What's Changed
  4 | 
  5 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.3.2...v0.4.0
  6 | <hr>
  7 | 
  8 | # v0.3.2 (2025-05-20)
  9 | 
 10 | ## What's Changed
 11 | 
 12 | * feat: update command arguments and add new client configurations for Trae and Trae CN by @cfc4n
 13 |   in https://github.com/gojue/moling/pull/35
 14 | * feat: update command arguments and improve logging for service initialization by @cfc4n
 15 |   in https://github.com/gojue/moling/pull/37
 16 | 
 17 | 
 18 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.3.1...v0.3.2
 19 | <hr>
 20 | 
 21 | # v0.3.1 (2025-04-22)
 22 | 
 23 | ## What's Changed
 24 | 
 25 | * fix: handle parent process exit to ensure MCP Server shutdown by @cfc4n
 26 |   in [#33](https://github.com/gojue/moling/pull/33)
 27 | * feat: update default prompts for browser, command, and filesystem modules by @cfc4n
 28 |   in [#34](https://github.com/gojue/moling/pull/34)
 29 | 
 30 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.3.0...v0.3.1
 31 | <hr>
 32 | 
 33 | # v0.3.0 (2025-04-18)
 34 | 
 35 | ## What's Changed
 36 | 
 37 | * feat: add client configuration paths for Trae and update README by @cfc4n
 38 |   in [#27](https://github.com/gojue/moling/pull/27)
 39 | * feat: support Cursor IDE (MCP Client) by @cfc4n in [#28](https://github.com/gojue/moling/pull/28)
 40 | * feat: Improves code consistency, adds PID management, and enhances user prompts by @cfc4n
 41 |   in [#29](https://github.com/gojue/moling/pull/29)
 42 | 
 43 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.2.0...v0.3.0
 44 | <hr>
 45 | 
 46 | # v0.2.0 (2025-04-13)
 47 | 
 48 | ## What's Changed
 49 | 
 50 | * feat: add support for advanced logging configuration by @cfc4n in [#25](https://github.com/gojue/moling/pull/25)
 51 | * fix: resolve issue with incorrect breakpoint handling by @cfc4n in [#26](https://github.com/gojue/moling/pull/26)
 52 | * docs: update README with debugging examples by @cfc4n in [#27](https://github.com/gojue/moling/pull/27)
 53 | 
 54 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.1.1...v0.2.0
 55 | <hr>
 56 | 
 57 | # v0.1.1 (2025-04-08)
 58 | ## What's Changed
 59 | 
 60 | * feat: update CLI descriptions for clarity and accuracy by @cfc4n in https://github.com/gojue/moling/pull/17
 61 | * refactor: change log levels from Info to Debug and Warn for improved … by @cfc4n
 62 |   in https://github.com/gojue/moling/pull/19
 63 | * fix: update README files to replace image links with direct URLs by @cfc4n in https://github.com/gojue/moling/pull/20
 64 | * feat: add MCP Client configuration to install script by @cfc4n in https://github.com/gojue/moling/pull/22
 65 | 
 66 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.1.0...v0.1.1
 67 | <hr>
 68 | 
 69 | # v0.1.0 (2025-04-04)
 70 | 
 71 | ## What's Changed
 72 | 
 73 | * Improve builder by @cfc4n in https://github.com/gojue/moling/pull/16
 74 | 
 75 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.0.8...v0.1.0
 76 | <hr>
 77 | 
 78 | # v0.0.8 (2025-04-04)
 79 | 
 80 | ## What's Changed
 81 | 
 82 | * docs: add Japanese README file by @eltociear in https://github.com/gojue/moling/pull/13
 83 | * feat: implement log file rotation and enhance logging configuration by @cfc4n
 84 |   in https://github.com/gojue/moling/pull/14
 85 | * chore: update dependencies for chromedp and mcp-go to latest versions by @cfc4n
 86 |   in https://github.com/gojue/moling/pull/15
 87 | 
 88 | ## New Contributors
 89 | 
 90 | * @eltociear made their first contribution in https://github.com/gojue/moling/pull/13
 91 | 
 92 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.0.7...v0.0.8
 93 | <hr>
 94 | 
 95 | # v0.0.7 (2025-03-31)
 96 | 
 97 | ## What's Changed
 98 | 
 99 | * feat: add logging enhancements and listen address flag for SSE mode by @cfc4n
100 |   in https://github.com/gojue/moling/pull/11
101 | * feat: add client command for automated MCP Server configuration management by @cfc4n
102 |   in https://github.com/gojue/moling/pull/12
103 | 
104 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.0.6...v0.0.7
105 | <hr>
106 | 
107 | # v0.0.6 (2025-03-30)
108 | 
109 | ## What's Changed
110 | 
111 | * feat: enhance service initialization and configuration loading by @cfc4n in https://github.com/gojue/moling/pull/9
112 | * feat: update installation script and README for improved user experie… by @cfc4n
113 |   in https://github.com/gojue/moling/pull/10
114 | 
115 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.0.5...v0.0.6
116 | <hr>
117 | 
118 | # v0.0.5 (2025-03-29)
119 | ## What's Changed
120 | 
121 | * fix #4 invalid data path by @cfc4n in https://github.com/gojue/moling/pull/5
122 | * feat: add automated release notes generation in release workflow by @cfc4n in https://github.com/gojue/moling/pull/6
123 | * Improve logging & error handling, rename configs, simplify service logic by @cfc4n
124 |   in https://github.com/gojue/moling/pull/8
125 | 
126 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.0.3...v0.0.5
127 | <hr>
128 | 
129 | # v0.0.3 (2025-03-28)
130 | ## What's Changed
131 | 
132 | * fix: build failed with GOOS=windows by @cfc4n in https://github.com/gojue/moling/pull/1
133 | * feat: refactor server initialization and directory creation logic by @cfc4n in https://github.com/gojue/moling/pull/2
134 | * feat: enhance configuration management and logging in the MoLing server by @cfc4n
135 |   in https://github.com/gojue/moling/pull/3
136 | 
137 | ## New Contributors
138 | 
139 | * @cfc4n made their first contribution in https://github.com/gojue/moling/pull/1
140 | 
141 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.0.2...v0.0.3
142 | <hr>
143 | 
144 | # v0.0.2 (2025-03-27)
145 | 
146 | ## What's Changed
147 | - Enhanced command validation with global configuration and support for piped commands
148 | - Added installation scripts for Windows/Linux and updated documentation
149 | - Implemented directory creation checks for base path and subdirectories
150 | - Corrected spelling of "MoLing" and "macOS" in documentation
151 | - Updated filename format for saved images
152 | - Refactored configuration management to use BasePath and improved browser initialization
153 | - Added listen address configuration and improved server initialization
154 | - Enhanced configuration management and logging
155 | - Added configuration management and service registration
156 | - Added browser service with logging
157 | - Updated GitHub links in README for accurate repository references
158 | 
159 | **Full Changelog**: https://github.com/gojue/moling/compare/v0.0.1...v0.0.2
160 | <hr>
161 | 
162 | # v0.0.1 (2025-03-23)
163 | 
164 | ## What's Changed
165 | 1. support `filesystem` service
166 | 2. support `command line` service
167 | 3. add GitHub action workflow
168 | 
169 | 
```

--------------------------------------------------------------------------------
/pkg/services/command/command.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | // Package services Description: This file contains the implementation of the CommandServer interface for macOS and  Linux.
 18 | package command
 19 | 
 20 | import (
 21 | 	"context"
 22 | 	"encoding/json"
 23 | 	"fmt"
 24 | 	"path/filepath"
 25 | 	"strings"
 26 | 
 27 | 	"github.com/mark3labs/mcp-go/mcp"
 28 | 	"github.com/rs/zerolog"
 29 | 
 30 | 	"github.com/gojue/moling/pkg/comm"
 31 | 	"github.com/gojue/moling/pkg/config"
 32 | 	"github.com/gojue/moling/pkg/services/abstract"
 33 | 	"github.com/gojue/moling/pkg/utils"
 34 | )
 35 | 
 36 | var (
 37 | 	// ErrCommandNotFound is returned when the command is not found.
 38 | 	ErrCommandNotFound = fmt.Errorf("command not found")
 39 | 	// ErrCommandNotAllowed is returned when the command is not allowed.
 40 | 	ErrCommandNotAllowed = fmt.Errorf("command not allowed")
 41 | )
 42 | 
 43 | const (
 44 | 	CommandServerName comm.MoLingServerType = "Command"
 45 | )
 46 | 
 47 | // CommandServer implements the Service interface and provides methods to execute named commands.
 48 | type CommandServer struct {
 49 | 	abstract.MLService
 50 | 	config    *CommandConfig
 51 | 	osName    string
 52 | 	osVersion string
 53 | }
 54 | 
 55 | // NewCommandServer creates a new CommandServer with the given allowed commands.
 56 | func NewCommandServer(ctx context.Context) (abstract.Service, error) {
 57 | 	var err error
 58 | 	cc := NewCommandConfig()
 59 | 	gConf, ok := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)
 60 | 	if !ok {
 61 | 		return nil, fmt.Errorf("CommandServer: invalid config type")
 62 | 	}
 63 | 
 64 | 	lger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)
 65 | 	if !ok {
 66 | 		return nil, fmt.Errorf("CommandServer: invalid logger type")
 67 | 	}
 68 | 
 69 | 	loggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
 70 | 		e.Str("Service", string(CommandServerName))
 71 | 	})
 72 | 
 73 | 	cs := &CommandServer{
 74 | 		MLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), gConf),
 75 | 		config:    cc,
 76 | 	}
 77 | 
 78 | 	err = cs.InitResources()
 79 | 	if err != nil {
 80 | 		return nil, err
 81 | 	}
 82 | 
 83 | 	return cs, nil
 84 | }
 85 | 
 86 | func (cs *CommandServer) Init() error {
 87 | 	var err error
 88 | 	pe := abstract.PromptEntry{
 89 | 		PromptVar: mcp.Prompt{
 90 | 			Name:        "command_prompt",
 91 | 			Description: "get command prompt",
 92 | 			//Arguments:   make([]mcp.PromptArgument, 0),
 93 | 		},
 94 | 		HandlerFunc: cs.handlePrompt,
 95 | 	}
 96 | 	cs.AddPrompt(pe)
 97 | 	cs.AddTool(mcp.NewTool(
 98 | 		"execute_command",
 99 | 		mcp.WithDescription("Execute a named command.Only support command execution on macOS and will strictly follow safety guidelines, ensuring that commands are safe and secure"),
100 | 		mcp.WithString("command",
101 | 			mcp.Description("The command to execute"),
102 | 			mcp.Required(),
103 | 		),
104 | 	), cs.handleExecuteCommand)
105 | 	return err
106 | }
107 | 
108 | func (cs *CommandServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
109 | 	return &mcp.GetPromptResult{
110 | 		Description: "",
111 | 		Messages: []mcp.PromptMessage{
112 | 			{
113 | 				Role: mcp.RoleUser,
114 | 				Content: mcp.TextContent{
115 | 					Type: "text",
116 | 					Text: fmt.Sprintf(cs.config.prompt, cs.MlConfig().SystemInfo),
117 | 				},
118 | 			},
119 | 		},
120 | 	}, nil
121 | }
122 | 
123 | // handleExecuteCommand handles the execution of a named command.
124 | func (cs *CommandServer) handleExecuteCommand(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
125 | 	args := request.GetArguments()
126 | 	command, ok := args["command"].(string)
127 | 	if !ok {
128 | 		return mcp.NewToolResultError(fmt.Errorf("command must be a string").Error()), nil
129 | 	}
130 | 
131 | 	// Check if the command is allowed
132 | 	if !cs.isAllowedCommand(command) {
133 | 		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))
134 | 		return mcp.NewToolResultError(fmt.Sprintf("Error: Command '%s' is not allowed", command)), nil
135 | 	}
136 | 
137 | 	// Execute the command
138 | 	output, err := ExecCommand(command)
139 | 	if err != nil {
140 | 		return mcp.NewToolResultError(fmt.Sprintf("Error executing command: %v", err)), nil
141 | 	}
142 | 
143 | 	return mcp.NewToolResultText(output), nil
144 | }
145 | 
146 | // isAllowedCommand checks if the command is allowed based on the configuration.
147 | func (cs *CommandServer) isAllowedCommand(command string) bool {
148 | 	// 检查命令是否在允许的列表中
149 | 	for _, allowed := range cs.config.allowedCommands {
150 | 		if strings.HasPrefix(command, allowed) {
151 | 			return true
152 | 		}
153 | 	}
154 | 
155 | 	// 如果命令包含管道符,进一步检查每个子命令
156 | 	if strings.Contains(command, "|") {
157 | 		parts := strings.Split(command, "|")
158 | 		for _, part := range parts {
159 | 			part = strings.TrimSpace(part)
160 | 			if !cs.isAllowedCommand(part) {
161 | 				return false
162 | 			}
163 | 		}
164 | 		return true
165 | 	}
166 | 
167 | 	if strings.Contains(command, "&") {
168 | 		parts := strings.Split(command, "&")
169 | 		for _, part := range parts {
170 | 			part = strings.TrimSpace(part)
171 | 			if !cs.isAllowedCommand(part) {
172 | 				return false
173 | 			}
174 | 		}
175 | 		return true
176 | 	}
177 | 
178 | 	return false
179 | }
180 | 
181 | // Config returns the configuration of the service as a string.
182 | func (cs *CommandServer) Config() string {
183 | 	cs.config.AllowedCommand = strings.Join(cs.config.allowedCommands, ",")
184 | 	cfg, err := json.Marshal(cs.config)
185 | 	if err != nil {
186 | 		cs.Logger.Err(err).Msg("failed to marshal config")
187 | 		return "{}"
188 | 	}
189 | 	cs.Logger.Debug().Str("config", string(cfg)).Msg("CommandServer config")
190 | 	return string(cfg)
191 | }
192 | 
193 | func (cs *CommandServer) Name() comm.MoLingServerType {
194 | 	return CommandServerName
195 | }
196 | 
197 | func (cs *CommandServer) Close() error {
198 | 	// Cancel the context to stop the browser
199 | 	cs.Logger.Debug().Msg("CommandServer closed")
200 | 	return nil
201 | }
202 | 
203 | // LoadConfig loads the configuration from a JSON object.
204 | func (cs *CommandServer) LoadConfig(jsonData map[string]any) error {
205 | 	err := utils.MergeJSONToStruct(cs.config, jsonData)
206 | 	if err != nil {
207 | 		return err
208 | 	}
209 | 	// split the AllowedCommand string into a slice
210 | 	cs.config.allowedCommands = strings.Split(cs.config.AllowedCommand, ",")
211 | 	return cs.config.Check()
212 | }
213 | 
```

--------------------------------------------------------------------------------
/pkg/services/browser/browser_debugger.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | // Package services provides a set of services for the MoLing application.
 18 | package browser
 19 | 
 20 | import (
 21 | 	"context"
 22 | 	"encoding/json"
 23 | 	"fmt"
 24 | 
 25 | 	"github.com/chromedp/cdproto/target"
 26 | 	"github.com/chromedp/chromedp"
 27 | 	"github.com/mark3labs/mcp-go/mcp"
 28 | )
 29 | 
 30 | // handleDebugEnable handles the enabling and disabling of debugging in the browser.
 31 | func (bs *BrowserServer) handleDebugEnable(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 32 | 	args := request.GetArguments()
 33 | 	enabled, ok := args["enabled"].(bool)
 34 | 	if !ok {
 35 | 		return mcp.NewToolResultError("enabled must be a boolean"), nil
 36 | 	}
 37 | 
 38 | 	var err error
 39 | 	rctx, cancel := context.WithCancel(bs.Context)
 40 | 	defer cancel()
 41 | 
 42 | 	if enabled {
 43 | 		err = chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
 44 | 			t := chromedp.FromContext(ctx).Target
 45 | 			// 使用Execute方法执行AttachToTarget命令
 46 | 			params := target.AttachToTarget(t.TargetID).WithFlatten(true)
 47 | 			return t.Execute(ctx, "Target.attachToTarget", params, nil)
 48 | 		}))
 49 | 	} else {
 50 | 		err = chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
 51 | 			t := chromedp.FromContext(ctx).Target
 52 | 			// 使用Execute方法执行DetachFromTarget命令
 53 | 			params := target.DetachFromTarget().WithSessionID(t.SessionID)
 54 | 			return t.Execute(ctx, "Target.detachFromTarget", params, nil)
 55 | 		}))
 56 | 	}
 57 | 
 58 | 	if err != nil {
 59 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to %s debugging: %v",
 60 | 			map[bool]string{true: "enable", false: "disable"}[enabled], err)), nil
 61 | 	}
 62 | 	return mcp.NewToolResultText(fmt.Sprintf("Debugging %s",
 63 | 		map[bool]string{true: "enabled", false: "disabled"}[enabled])), nil
 64 | }
 65 | 
 66 | // handleSetBreakpoint handles setting a breakpoint in the browser.
 67 | func (bs *BrowserServer) handleSetBreakpoint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 68 | 	args := request.GetArguments()
 69 | 	url, ok := args["url"].(string)
 70 | 	if !ok {
 71 | 		return mcp.NewToolResultError("url must be a string"), nil
 72 | 	}
 73 | 
 74 | 	line, ok := args["line"].(float64)
 75 | 	if !ok {
 76 | 		return mcp.NewToolResultError("line must be a number"), nil
 77 | 	}
 78 | 
 79 | 	column, _ := args["column"].(float64)
 80 | 	condition, _ := args["condition"].(string)
 81 | 
 82 | 	var breakpointID string
 83 | 	rctx, cancel := context.WithCancel(bs.Context)
 84 | 	defer cancel()
 85 | 	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
 86 | 		t := chromedp.FromContext(ctx).Target
 87 | 		params := map[string]any{
 88 | 			"url":       url,
 89 | 			"line":      int(line),
 90 | 			"column":    int(column),
 91 | 			"condition": condition,
 92 | 		}
 93 | 
 94 | 		var result map[string]any
 95 | 		// 使用Execute方法执行Debugger.setBreakpoint命令
 96 | 		if err := t.Execute(ctx, "Debugger.setBreakpoint", params, &result); err != nil {
 97 | 			return err
 98 | 		}
 99 | 
100 | 		breakpointID, ok = result["breakpointId"].(string)
101 | 		if !ok {
102 | 			breakpointID = ""
103 | 			return fmt.Errorf("failed to get breakpoint ID")
104 | 		}
105 | 		return nil
106 | 	}))
107 | 
108 | 	if err != nil {
109 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to set breakpoint: %s", err.Error())), nil
110 | 	}
111 | 	return mcp.NewToolResultText(fmt.Sprintf("Breakpoint set with ID: %s", breakpointID)), nil
112 | }
113 | 
114 | // handleRemoveBreakpoint handles removing a breakpoint in the browser.
115 | func (bs *BrowserServer) handleRemoveBreakpoint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
116 | 	args := request.GetArguments()
117 | 	breakpointID, ok := args["breakpointId"].(string)
118 | 	if !ok {
119 | 		return mcp.NewToolResultError("breakpointId must be a string"), nil
120 | 	}
121 | 	rctx, cancel := context.WithCancel(bs.Context)
122 | 	defer cancel()
123 | 	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
124 | 		t := chromedp.FromContext(ctx).Target
125 | 		// 使用Execute方法执行Debugger.removeBreakpoint命令
126 | 		return t.Execute(ctx, "Debugger.removeBreakpoint", map[string]any{
127 | 			"breakpointId": breakpointID,
128 | 		}, nil)
129 | 	}))
130 | 
131 | 	if err != nil {
132 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to remove breakpoint: %s", err.Error())), nil
133 | 	}
134 | 	return mcp.NewToolResultText(fmt.Sprintf("Breakpoint %s removed", breakpointID)), nil
135 | }
136 | 
137 | // handlePause handles pausing the JavaScript execution in the browser.
138 | func (bs *BrowserServer) handlePause(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
139 | 	rctx, cancel := context.WithCancel(bs.Context)
140 | 	defer cancel()
141 | 	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
142 | 		t := chromedp.FromContext(ctx).Target
143 | 		// 使用Execute方法执行Debugger.pause命令
144 | 		return t.Execute(ctx, "Debugger.pause", nil, nil)
145 | 	}))
146 | 
147 | 	if err != nil {
148 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to pause execution: %s", err.Error())), nil
149 | 	}
150 | 	return mcp.NewToolResultText("JavaScript execution paused"), nil
151 | }
152 | 
153 | // handleResume handles resuming the JavaScript execution in the browser.
154 | func (bs *BrowserServer) handleResume(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
155 | 	rctx, cancel := context.WithCancel(bs.Context)
156 | 	defer cancel()
157 | 	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
158 | 		t := chromedp.FromContext(ctx).Target
159 | 		// 使用Execute方法执行Debugger.resume命令
160 | 		return t.Execute(ctx, "Debugger.resume", nil, nil)
161 | 	}))
162 | 
163 | 	if err != nil {
164 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to resume execution: %s", err.Error())), nil
165 | 	}
166 | 	return mcp.NewToolResultText("JavaScript execution resumed"), nil
167 | }
168 | 
169 | // handleStepOver handles stepping over the next line of JavaScript code in the browser.
170 | func (bs *BrowserServer) handleGetCallstack(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
171 | 	var callstack any
172 | 	rctx, cancel := context.WithCancel(bs.Context)
173 | 	defer cancel()
174 | 	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
175 | 		t := chromedp.FromContext(ctx).Target
176 | 		// 使用Execute方法执行Debugger.getStackTrace命令
177 | 		return t.Execute(ctx, "Debugger.getStackTrace", nil, &callstack)
178 | 	}))
179 | 
180 | 	if err != nil {
181 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to get call stack: %s", err.Error())), nil
182 | 	}
183 | 
184 | 	callstackJSON, err := json.Marshal(callstack)
185 | 	if err != nil {
186 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to marshal call stack: %s", err.Error())), nil
187 | 	}
188 | 
189 | 	return mcp.NewToolResultText(fmt.Sprintf("Current call stack: %s", string(callstackJSON))), nil
190 | }
191 | 
```
Page 1/2FirstPrevNextLast