Full Code of gojue/moling for AI

master ae07f704cbc8 cached
62 files
222.3 KB
64.0k tokens
207 symbols
1 requests
Download .txt
Showing preview only (246K chars total). Download the full file or copy to clipboard to get everything.
Repository: gojue/moling
Branch: master
Commit: ae07f704cbc8
Files: 62
Total size: 222.3 KB

Directory structure:
gitextract_rez5qvem/

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

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/go-test.yml
================================================
name: Golang CI and Test

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build-on-ubuntu2204:
    strategy:
      matrix:
        os: [ darwin, windows, linux ]
        arch: [amd64, arm64]
    runs-on: ubuntu-22.04
    name: test on ${{ matrix.os }} ${{ matrix.arch }}
    steps:
      - uses: actions/setup-go@v5
        with:
          go-version: '1.24.1'
      - uses: actions/checkout@v4
        with:
          submodules: 'recursive'
          fetch-depth: 0
      - name: golangci-lint
        uses: golangci/golangci-lint-action@v8
        with:
          version: v2.1
          skip-cache: true
          problem-matchers: true
      - name: Test (go test)
        run: |
          make clean
          TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env
          TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make test
      - name: MoLing Build
        run: |
          make clean
          TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env
          TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make build

================================================
FILE: .github/workflows/release.yml
================================================
name: MoLing Release
on:
  push:
    tags:
      - "v*"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-on-ubuntu2204:
    strategy:
      matrix:
        os: [ darwin, windows, linux ]
        arch: [ amd64, arm64 ]
    runs-on: ubuntu-22.04
    name: Release on ${{ matrix.os }} ${{ matrix.arch }}
    steps:
      - uses: actions/setup-go@v5
        with:
          go-version: '1.24.1'
      - uses: actions/checkout@v4
        with:
          submodules: 'recursive'
          fetch-depth: 0
      - name: MoLing Build
        run: |
          make clean
          SNAPSHOT_VERSION=${{ github.ref_name }} TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env
          SNAPSHOT_VERSION=${{ github.ref_name }} TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make build
          pwd
          ls -al ./bin
      - name: Create Archive
        run: |
          mkdir -p ./dist
          pwd
          ls -al ./bin
          if [ "${{ matrix.os }}" = "windows" ]; then
            cd ./bin && zip -qr ./../dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.zip ./bin/ . && cd ..
          else
            tar -czvf dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz -C ./bin/ .
          fi
      - name: Upload Release Asset
        uses: softprops/action-gh-release@v2
        if: startsWith(github.ref, 'refs/tags/')
        with:
          tag_name: ${{ github.ref_name }}
          generate_release_notes: true
          files: |
            ./dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.*

================================================
FILE: .gitignore
================================================
/services/mo*
/bin/moling

================================================
FILE: .golangci.yml
================================================
# This configuration file is not a recommendation.
#
# We intentionally use a limited set of linters.
# This configuration file is used with different version of golangci-lint to avoid regressions:
# the linters can change between version,
# their configuration may be not compatible or their reports can be different,
# and this can break some of our tests.
# Also, some linters are not relevant for the project (e.g. linters related to SQL).
#
# We have specific constraints, so we use a specific configuration.
#
# See the file `.golangci.reference.yml` to have a list of all available configuration options.

version: "2"

linters:
  default: none
  # This list of linters is not a recommendation (same thing for all this configuration file).
  # We intentionally use a limited set of linters.
  # See the comment on top of this file.
  enable:
    - errcheck
    - staticcheck
    - errorlint

  settings:
    errorlint:
      asserts: false
    staticcheck:
      checks:
        # Invalid regular expression.
        # https://staticcheck.dev/docs/checks/#SA1000
        - all
        - "-ST1000"
        - "-S1023"
        - "-S1005"
        - "-QF1004"

  exclusions:
    paths:
      - dist/
      - docs/
      - tests/
      - bin/
      - images/
      - install/
      - utils/

formatters:
  enable:
    - gofmt
    - goimports
  settings:
    gofmt:
      rewrite-rules:
        - pattern: 'interface{}'
          replacement: 'any'
    goimports:
      local-prefixes:
        - github.com/gojue/moling
  exclusions:
    paths:
      - dist/
      - docs/
      - tests/
      - bin/
      - images/
      - install/
      - utils/


================================================
FILE: CHANGELOG.md
================================================
# v0.4.0 (2025-05-31)

## What's Changed

**Full Changelog**: https://github.com/gojue/moling/compare/v0.3.2...v0.4.0
<hr>

# v0.3.2 (2025-05-20)

## What's Changed

* feat: update command arguments and add new client configurations for Trae and Trae CN by @cfc4n
  in https://github.com/gojue/moling/pull/35
* feat: update command arguments and improve logging for service initialization by @cfc4n
  in https://github.com/gojue/moling/pull/37


**Full Changelog**: https://github.com/gojue/moling/compare/v0.3.1...v0.3.2
<hr>

# v0.3.1 (2025-04-22)

## What's Changed

* fix: handle parent process exit to ensure MCP Server shutdown by @cfc4n
  in [#33](https://github.com/gojue/moling/pull/33)
* feat: update default prompts for browser, command, and filesystem modules by @cfc4n
  in [#34](https://github.com/gojue/moling/pull/34)

**Full Changelog**: https://github.com/gojue/moling/compare/v0.3.0...v0.3.1
<hr>

# v0.3.0 (2025-04-18)

## What's Changed

* feat: add client configuration paths for Trae and update README by @cfc4n
  in [#27](https://github.com/gojue/moling/pull/27)
* feat: support Cursor IDE (MCP Client) by @cfc4n in [#28](https://github.com/gojue/moling/pull/28)
* feat: Improves code consistency, adds PID management, and enhances user prompts by @cfc4n
  in [#29](https://github.com/gojue/moling/pull/29)

**Full Changelog**: https://github.com/gojue/moling/compare/v0.2.0...v0.3.0
<hr>

# v0.2.0 (2025-04-13)

## What's Changed

* feat: add support for advanced logging configuration by @cfc4n in [#25](https://github.com/gojue/moling/pull/25)
* fix: resolve issue with incorrect breakpoint handling by @cfc4n in [#26](https://github.com/gojue/moling/pull/26)
* docs: update README with debugging examples by @cfc4n in [#27](https://github.com/gojue/moling/pull/27)

**Full Changelog**: https://github.com/gojue/moling/compare/v0.1.1...v0.2.0
<hr>

# v0.1.1 (2025-04-08)
## What's Changed

* feat: update CLI descriptions for clarity and accuracy by @cfc4n in https://github.com/gojue/moling/pull/17
* refactor: change log levels from Info to Debug and Warn for improved … by @cfc4n
  in https://github.com/gojue/moling/pull/19
* fix: update README files to replace image links with direct URLs by @cfc4n in https://github.com/gojue/moling/pull/20
* feat: add MCP Client configuration to install script by @cfc4n in https://github.com/gojue/moling/pull/22

**Full Changelog**: https://github.com/gojue/moling/compare/v0.1.0...v0.1.1
<hr>

# v0.1.0 (2025-04-04)

## What's Changed

* Improve builder by @cfc4n in https://github.com/gojue/moling/pull/16

**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.8...v0.1.0
<hr>

# v0.0.8 (2025-04-04)

## What's Changed

* docs: add Japanese README file by @eltociear in https://github.com/gojue/moling/pull/13
* feat: implement log file rotation and enhance logging configuration by @cfc4n
  in https://github.com/gojue/moling/pull/14
* chore: update dependencies for chromedp and mcp-go to latest versions by @cfc4n
  in https://github.com/gojue/moling/pull/15

## New Contributors

* @eltociear made their first contribution in https://github.com/gojue/moling/pull/13

**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.7...v0.0.8
<hr>

# v0.0.7 (2025-03-31)

## What's Changed

* feat: add logging enhancements and listen address flag for SSE mode by @cfc4n
  in https://github.com/gojue/moling/pull/11
* feat: add client command for automated MCP Server configuration management by @cfc4n
  in https://github.com/gojue/moling/pull/12

**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.6...v0.0.7
<hr>

# v0.0.6 (2025-03-30)

## What's Changed

* feat: enhance service initialization and configuration loading by @cfc4n in https://github.com/gojue/moling/pull/9
* feat: update installation script and README for improved user experie… by @cfc4n
  in https://github.com/gojue/moling/pull/10

**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.5...v0.0.6
<hr>

# v0.0.5 (2025-03-29)
## What's Changed

* fix #4 invalid data path by @cfc4n in https://github.com/gojue/moling/pull/5
* feat: add automated release notes generation in release workflow by @cfc4n in https://github.com/gojue/moling/pull/6
* Improve logging & error handling, rename configs, simplify service logic by @cfc4n
  in https://github.com/gojue/moling/pull/8

**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.3...v0.0.5
<hr>

# v0.0.3 (2025-03-28)
## What's Changed

* fix: build failed with GOOS=windows by @cfc4n in https://github.com/gojue/moling/pull/1
* feat: refactor server initialization and directory creation logic by @cfc4n in https://github.com/gojue/moling/pull/2
* feat: enhance configuration management and logging in the MoLing server by @cfc4n
  in https://github.com/gojue/moling/pull/3

## New Contributors

* @cfc4n made their first contribution in https://github.com/gojue/moling/pull/1

**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.2...v0.0.3
<hr>

# v0.0.2 (2025-03-27)

## What's Changed
- Enhanced command validation with global configuration and support for piped commands
- Added installation scripts for Windows/Linux and updated documentation
- Implemented directory creation checks for base path and subdirectories
- Corrected spelling of "MoLing" and "macOS" in documentation
- Updated filename format for saved images
- Refactored configuration management to use BasePath and improved browser initialization
- Added listen address configuration and improved server initialization
- Enhanced configuration management and logging
- Added configuration management and service registration
- Added browser service with logging
- Updated GitHub links in README for accurate repository references

**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.1...v0.0.2
<hr>

# v0.0.1 (2025-03-23)

## What's Changed
1. support `filesystem` service
2. support `command line` service
3. add GitHub action workflow



================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright CFC4N [cfc4ncs@gmail.com]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

================================================
FILE: Makefile
================================================
include variables.mk
include functions.mk

.PHONY: all | env
all: clean build
	@echo $(shell date)

.ONESHELL:
SHELL = /bin/bash

.PHONY: env
env:
	@echo ---------------------------------------
	@echo "MoLing Makefile Environment:"
	@echo ---------------------------------------
	@echo "SNAPSHOT_VERSION         $(SNAPSHOT_VERSION)"
	@echo ---------------------------------------
	@echo "OS_NAME                  $(OS_NAME)"
	@echo "OS_ARCH                  $(OS_ARCH)"
	@echo "TARGET_OS                $(TARGET_OS)"
	@echo "TARGET_ARCH              $(TARGET_ARCH)"
	@echo "GO_VERSION               $(GO_VERSION)"
	@echo ---------------------------------------
	@echo "CMD_GIT                  $(CMD_GIT)"
	@echo "CMD_GO                   $(CMD_GO)"
	@echo "CMD_INSTALL              $(CMD_INSTALL)"
	@echo "CMD_MD5                  $(CMD_MD5)"
	@echo ---------------------------------------
	@echo "VERSION_NUM              $(VERSION_NUM)"
	@echo "LAST_GIT_TAG             $(LAST_GIT_TAG)"
	@echo ---------------------------------------


.PHONY: help
help:
	@echo "# environment"
	@echo "    $$ make env					# show makefile environment/variables"
	@echo ""
	@echo "# build"
	@echo "    $$ make all					# build MoLing"
	@echo ""
	@echo "# clean"
	@echo "    $$ make clean				# wipe ./bin/"
	@echo ""
	@echo "# test"

.PHONY: clean build

.PHONY: clean
clean:
	$(CMD_RM) -f $(OUT_BIN)*

.PHONY: build
build:clean
	$(call gobuild,$(TARGET_OS),$(TARGET_ARCH))

# Format the code
.PHONY: format
format:
	@echo "  ->  Formatting code"
	golangci-lint run --disable-all -E errcheck -E staticcheck

.PHONY: test
test:
	CGO_ENABLED=1 go test -v -race ./...


================================================
FILE: Makefile.release
================================================
include variables.mk
include functions.mk

.PHONY: all build package clean

# 定义目标系统和架构
TARGET_SYSTEMS := darwin linux windows
TARGET_ARCHS := amd64 arm64

# 输出目录
DIST_DIR := dist

all: build

# 遍历系统和架构,编译程序
build:
	@for os in $(TARGET_SYSTEMS); do \
		for arch in $(TARGET_ARCHS); do \
			echo "Building for $$os/$$arch..."; \
			$(MAKE) TARGET_OS=$$os TARGET_ARCH=$$arch ; \
			echo "Packaging for $$os/$$arch..."; \
			OUT_DIR=moling\_$$os\_$$arch\_$(COMMIT); \
			mkdir -p $(DIST_DIR)/$$OUT_DIR; \
			cp bin/moling$$([ "$$os" = "windows" ] && echo ".exe") $(DIST_DIR)/$$OUT_DIR/; \
			if [ "$$os" = "windows" ]; then \
				(cd $(DIST_DIR) && zip -r $$OUT_DIR.zip $$OUT_DIR); \
            else \
				(cd $(DIST_DIR) && tar -czf $$OUT_DIR.tar.gz $$OUT_DIR); \
            fi; \
		done; \
	done

# 清理生成的文件
clean:
	$(CMD_RM) -rf bin/* $(DIST_DIR)/*

================================================
FILE: README.md
================================================
## MoLing MCP Server

English | [中文](./README_ZH_HANS.md) | [日本語](./README_JA_JP.md)

[![GitHub stars](https://img.shields.io/github/stars/gojue/moling.svg?label=Stars&logo=github)](https://github.com/gojue/moling/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/gojue/moling?label=Forks&logo=github)](https://github.com/gojue/moling/forks)
[![CI](https://github.com/gojue/moling/actions/workflows/go-test.yml/badge.svg)](https://github.com/gojue/moling/actions/workflows/go-test.yml)
[![Github Version](https://img.shields.io/github/v/release/gojue/moling?display_name=tag&include_prereleases&sort=semver)](https://github.com/gojue/moling/releases)

---

![](./images/logo.svg)

### Introduction
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.

### Advantages
> [!IMPORTANT]
> Requiring no installation of any dependencies, MoLing can be run directly and is compatible with multiple operating systems, including Windows, Linux, and macOS. 
> This eliminates the hassle of dealing with environment conflicts involving Node.js, Python, Docker and other development environments.

### Features

> [!CAUTION]
> Command-line operations are dangerous and should be used with caution.

- **File System Operations**: Reading, writing, merging, statistics, and aggregation
- **Command-line Terminal**: Execute system commands directly
- **Browser Control**: Powered by `github.com/chromedp/chromedp`
    - Chrome browser is required.
    - In Windows, the full path to Chrome needs to be configured in the system environment variables.
- **Future Plans**:
    - Personal PC data organization
    - Document writing assistance
    - Schedule planning
    - Life assistant features

> [!WARNING]
> Currently, MoLing has only been tested on macOS, and other operating systems may have issues.

### Supported MCP Clients

- [Claude](https://claude.ai/)
- [Cline](https://cline.bot/)
- [Cherry Studio](https://cherry-ai.com/)
- etc. (who support MCP protocol)

#### Demos

https://github.com/user-attachments/assets/229c4dd5-23b4-4b53-9e25-3eba8734b5b7

MoLing in [Claude](https://claude.ai/)
![](./images/screenshot_claude.png)

#### Configuration Format

##### MCP Server (MoLing) configuration

The configuration file will be generated at `/Users/username/.moling/config/config.json`, and you can modify its
contents as needed.

If the file does not exist, you can create it using `moling config --init`.

##### MCP Client configuration
For example, to configure the Claude client, add the following configuration:

> [!TIP]
> 
> Only 3-6 lines of configuration are needed.
> 
> Claude config path: `~/Library/Application\ Support/Claude/claude_desktop_config`

```json
{
  "mcpServers": {
    "MoLing": {
      "command": "/usr/local/bin/moling",
      "args": []
    }
  }
}
```

and, `/usr/local/bin/moling` is the path to the MoLing server binary you downloaded.

**Automatic Configuration**

run `moling client --install` to automatically install the configuration for the MCP client.

MoLing will automatically detect the MCP client and install the configuration for you. including: Cline, Claude, Roo
Code, etc.

### Operation Modes

- **Stdio Mode**: CLI-based interactive mode for user-friendly experience
- **SSE Mode**: Server-Side Rendering mode optimized for headless/automated environments

### Installation

#### Option 1: Install via Script
##### Linux/MacOS
```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.sh)"
```
##### Windows

> [!WARNING]
> Not tested, unsure if it works.

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.ps1 | iex"
```

#### Option 2: Direct Download
1. Download the installation package from [releases page](https://github.com/gojue/moling/releases)
2. Extract the package
3. Run the server:
   ```sh
   ./moling
   ```

#### Option 3: Build from Source
1. Clone the repository:
```sh
git clone https://github.com/gojue/moling.git
cd moling
```
2. Build the project (requires Golang toolchain):
```sh
make build
```
3. Run the compiled binary:
```sh
./bin/moling
```

### Usage
After starting the server, connect using any supported MCP client by configuring it to point to your MoLing server address.

### License
Apache License 2.0. See [LICENSE](LICENSE) for details.


================================================
FILE: README_JA_JP.md
================================================
## MoLing MCP サーバー

[English](./README.md) | [中文](./README_ZH_HANS.md) | 日本語

[![GitHub stars](https://img.shields.io/github/stars/gojue/moling.svg?label=Stars&logo=github)](https://github.com/gojue/moling/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/gojue/moling?label=Forks&logo=github)](https://github.com/gojue/moling/forks)
[![CI](https://github.com/gojue/moling/actions/workflows/go-test.yml/badge.svg)](https://github.com/gojue/moling/actions/workflows/go-test.yml)
[![Github Version](https://img.shields.io/github/v/release/gojue/moling?display_name=tag&include_prereleases&sort=semver)](https://github.com/gojue/moling/releases)

---

![](./images/logo.svg)

### 紹介
MoLingは、オペレーティングシステムAPIを介してシステム操作を実装するコンピュータ使用およびブラウザ使用のMCPサーバーであり、ファイルシステム操作(読み取り、書き込み、マージ、統計、集計)やシステムコマンドの実行を可能にします。依存関係のないローカルオフィス自動化アシスタントです。

### 利点
> [!IMPORTANT]
> 依存関係のインストールを必要とせず、MoLingは直接実行でき、Windows、Linux、macOSなどの複数のオペレーティングシステムと互換性があります。
> これにより、Node.js、Python、Dockerなどの開発環境に関連する環境の競合を処理する手間が省けます。

### 機能

> [!CAUTION]
> コマンドライン操作は危険であり、慎重に使用する必要があります。

- **ファイルシステム操作**:読み取り、書き込み、マージ、統計、集計
- **コマンドラインターミナル**:システムコマンドを直接実行
- **ブラウザ制御**:`github.com/chromedp/chromedp`によって提供される
    - Chromeブラウザのインストールが必要です。
    - Windows環境では、環境変数にChromeのフルパスを設定する必要があります。
- **将来の計画**:
    - 個人PCデータの整理
    - ドキュメント作成支援
    - スケジュール計画
    - 生活アシスタント機能

> [!WARNING]
> 現在、MoLingはmacOSでのみテストされており、他のオペレーティングシステムでは問題が発生する可能性があります。

### サポートされているMCPクライアント

- [Claude](https://claude.ai/)
- [Cline](https://cline.bot/)
- [Cherry Studio](https://cherry-ai.com/)
- その他(MCPプロトコルをサポートするクライアント)

#### スクリーンショット

https://github.com/user-attachments/assets/229c4dd5-23b4-4b53-9e25-3eba8734b5b7

[Claude](https://claude.ai/)に統合されたMoLing
![](./images/screenshot_claude.png)

#### 設定形式

##### MCPサーバー(MoLing)設定

設定ファイルは`/Users/username/.moling/config/config.json`に生成され、必要に応じて内容を変更できます。

ファイルが存在しない場合は、`moling config --init`を使用して作成できます。

##### MCPクライアント設定
例として、Claudeクライアントを設定するには、次の設定を追加します:

> [!TIP]
> 
> 3〜6行の設定のみが必要です。
> 
> Claude設定パス:`~/Library/Application\ Support/Claude/claude_desktop_config`

```json
{
  "mcpServers": {
    "MoLing": {
      "command": "/usr/local/bin/moling",
      "args": []
    }
  }
}
```

また、`/usr/local/bin/moling`はダウンロードしたMoLingサーバーバイナリのパスです。

**自動設定**

`moling client --install`を実行して、MCPクライアントの設定を自動的にインストールします。

MoLingはMCPクライアントを自動的に検出し、設定をインストールします。Cline、Claude、Roo Codeなどを含みます。

### 動作モード

- **Stdioモード**:CLIベースのインタラクティブモードで、ユーザーフレンドリーな体験を提供
- **SSEモード**:ヘッドレス/自動化環境に最適化されたサーバーサイドレンダリングモード

### インストール

#### オプション1:スクリプトを使用してインストール
##### Linux/MacOS
```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.sh)"
```
##### Windows

> [!WARNING]
> テストされていないため、動作するかどうかは不明です。

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.ps1 | iex"
```

#### オプション2:直接ダウンロード
1. [リリースページ](https://github.com/gojue/moling/releases)からインストールパッケージをダウンロード
2. パッケージを解凍
3. サーバーを実行:
   ```sh
   ./moling
   ```

#### オプション3:ソースからビルド
1. リポジトリをクローン:
```sh
git clone https://github.com/gojue/moling.git
cd moling
```
2. プロジェクトをビルド(Golangツールチェーンが必要):
```sh
make build
```
3. コンパイルされたバイナリを実行:
```sh
./bin/moling
```

### 使用方法
サーバーを起動した後、サポートされているMCPクライアントを使用して、MoLingサーバーアドレスに接続します。

### ライセンス
Apache License 2.0。詳細は[LICENSE](LICENSE)ファイルを参照してください。


================================================
FILE: README_ZH_HANS.md
================================================
## MoLing MCP 服务器

[English](./README.md) | 中文 | [日本語](./README_JA_JP.md)

[![GitHub stars](https://img.shields.io/github/stars/gojue/moling.svg?label=Stars&logo=github)](https://github.com/gojue/moling/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/gojue/moling?label=Forks&logo=github)](https://github.com/gojue/moling/forks)
[![CI](https://github.com/gojue/moling/actions/workflows/go-test.yml/badge.svg)](https://github.com/gojue/moling/actions/workflows/go-test.yml)
[![Github Version](https://img.shields.io/github/v/release/gojue/moling?display_name=tag&include_prereleases&sort=semver)](https://github.com/gojue/moling/releases)

---

![](./images/logo.svg)

### 简介
MoLing是一个computer-use和browser-use的MCP Server,基于操作系统API实现了系统交互,浏览器模拟控制,可以实现文件系统的读写、合并、统计、聚合等操作,也可以执行系统命令操作。是一个无需任何依赖的本地办公自动化助手。

### 优势
> [!IMPORTANT]
> 没有任何安装依赖,直接运行,兼容Windows、Linux、macOS等操作系统。
> 再也不用苦恼NodeJS、Python、Docker等环境冲突等问题。

### 功能特性

> [!CAUTION]
> 命令行操作具备一定风险性,且不可回滚,使用需谨慎,默认配置为只读的命令列表。

- **文件系统操作**:读取、写入、合并、统计和聚合
- **命令行终端**:直接执行系统命令
- **浏览器控制**:基于 `github.com/chromedp/chromedp`
  - 需要安装Chrome浏览器
  - Windows系统中,需要在环境变量中配置Chrome的完整路径
- **未来计划**:
    - 个人电脑资料整理
    - 文档编写辅助
    - 行程规划
    - 生活助手功能

> [!WARNING]
> 当前, MoLing仅在macOS测试通过,Linux和Windows未经验证。

### 支持的MCP客户端

- [Claude](https://claude.ai/)
- [Cline](https://cline.bot/)
- [Cherry Studio](https://cherry-ai.com/)
- 其他(支持MCP协议的客户端)

#### 演示

https://github.com/user-attachments/assets/229c4dd5-23b4-4b53-9e25-3eba8734b5b7

集成在[Claude](https://claude.ai/)中的MoLing
![](./images/screenshot_claude.png)

#### 配置格式

##### MCP Server(MoLing)配置

配置文件会生成在`/Users/username/.moling/config/config.json`下,你可以自行修改内容。若文件不存在,你可以通过
`moling config --init`创建它。

##### MCP Client配置
以Claude客户端为例,在配置文件中添加如下配置:

> [!TIP]
> 
> 仅需添加3-6行的配置。
> Claude配置文件路径:`~/Library/Application\ Support/Claude/claude_desktop_config`

```json
{
  "mcpServers": {
    "MoLing": {
      "command": "/usr/local/bin/moling",
      "args": []
    }
  }
}
```

另外, `/usr/local/bin/moling` 是你存放`MoLing` Server可执行文件的路径,可以自己指定。

**自动配置**

运行 `moling client --install` 命令将会自动为本机的所有MCP客户端安装MoLing。包括Cline、 Claude、 Roo Code等等。

### 运行模式

- **Stdio模式**:本地命令行交互模式,依赖于终端输入输出,适合人机交互
- **SSE模式**:远程通讯模式,适合远程部署,远程调用

### 安装指南


#### 方法一: 脚本安装
#### Linux/MacOS
```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.sh)"
```

##### Windows

> [!WARNING]
> 未测试,不确定是否正常。

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.ps1 | iex"
```


#### 方法二:直接下载
1. 从[发布页面](https://github.com/gojue/moling/releases)下载安装包
2. 解压安装包
3. 运行服务器:
```sh
./moling
```

#### 方法三:从源码编译
1. 克隆代码库:
```sh
git clone https://github.com/gojue/moling.git
cd moling
```
2. 编译项目(需要Golang工具链):
```sh
make build
```
3. 运行编译后的程序:
```sh
./bin/moling
```

### 使用说明
启动服务器后,使用任何支持的MCP客户端配置连接到您的MoLing服务器地址即可。

### 许可证
Apache License 2.0。详见[LICENSE](LICENSE)文件。


================================================
FILE: bin/.gitkeep
================================================


================================================
FILE: cli/cmd/client.go
================================================
/*
 *
 *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *  Repository: https://github.com/gojue/moling
 *
 */

package cmd

import (
	"os"
	"time"

	"github.com/rs/zerolog"
	"github.com/spf13/cobra"

	"github.com/gojue/moling/client"
)

var clientCmd = &cobra.Command{
	Use:   "client",
	Short: "Provides automated access to MoLing MCP Server for local MCP clients, Cline, Roo Code, and Claude, etc.",
	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.
Currently supports the following clients: Cline, Roo Code, Claude
    moling client -l --list   List the current installed MCP clients
    moling client -i --install Add MoLing MCP Server configuration to the currently installed MCP clients on this computer
`,
	RunE: ClientCommandFunc,
}

var (
	list    bool
	install bool
)

// ClientCommandFunc executes the "config" command.
func ClientCommandFunc(command *cobra.Command, args []string) error {
	logger := initLogger(mlConfig.BasePath)
	consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
	multi := zerolog.MultiLevelWriter(consoleWriter, logger)
	logger = zerolog.New(multi).With().Timestamp().Logger()
	mlConfig.SetLogger(logger)
	logger.Debug().Msg("Start to show MCP Clients")
	mcpConfig := client.NewMCPServerConfig(CliDescription, CliName, MCPServerName)
	exePath, err := os.Executable()
	if err == nil {
		logger.Debug().Str("exePath", exePath).Msg("executable path, will use this path to find the config file")
		mcpConfig.Command = exePath
	}
	cm := client.NewManager(logger, mcpConfig)
	if install {
		logger.Info().Msg("Start to add MCP Server configuration into MCP Clients.")
		cm.SetupConfig()
		logger.Info().Msg("Add MCP Server configuration into MCP Clients successfully.")
		return nil
	}
	logger.Info().Msg("Start to list MCP Clients")
	cm.ListClient()
	logger.Info().Msg("List MCP Clients successfully.")
	return nil
}

func init() {
	clientCmd.PersistentFlags().BoolVar(&list, "list", false, "List the current installed MCP clients")
	clientCmd.PersistentFlags().BoolVarP(&install, "install", "i", false, "Add MoLing MCP Server configuration to the currently installed MCP clients on this computer. default is all")
	rootCmd.AddCommand(clientCmd)
}


================================================
FILE: cli/cmd/config.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package cmd

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/rs/zerolog"
	"github.com/spf13/cobra"

	"github.com/gojue/moling/pkg/comm"
	"github.com/gojue/moling/pkg/services"
)

var configCmd = &cobra.Command{
	Use:   "config",
	Short: "Show the configuration of the current service list",
	Long: `Show the configuration of the current service list. You can refer to the configuration file to modify the configuration.
`,
	RunE: ConfigCommandFunc,
}

var (
	initial bool
)

// ConfigCommandFunc executes the "config" command.
func ConfigCommandFunc(command *cobra.Command, args []string) error {
	var err error
	logger := initLogger(mlConfig.BasePath)
	consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
	multi := zerolog.MultiLevelWriter(consoleWriter, logger)
	logger = zerolog.New(multi).With().Timestamp().Logger()
	mlConfig.SetLogger(logger)
	logger.Info().Msg("Start to show config")
	ctx := context.WithValue(context.Background(), comm.MoLingConfigKey, mlConfig)
	ctx = context.WithValue(ctx, comm.MoLingLoggerKey, logger)

	// 当前配置文件检测
	hasConfig := false
	var nowConfig []byte
	nowConfigJSON := make(map[string]any)
	configFilePath := filepath.Join(mlConfig.BasePath, mlConfig.ConfigFile)
	if nowConfig, err = os.ReadFile(configFilePath); err == nil {
		hasConfig = true
	}
	if hasConfig {
		err = json.Unmarshal(nowConfig, &nowConfigJSON)
		if err != nil {
			return fmt.Errorf("error unmarshaling JSON: %w, payload:%s", err, string(nowConfig))
		}
	}

	bf := bytes.Buffer{}
	bf.WriteString("\n{\n")

	// 写入GlobalConfig
	mlConfigJSON, err := json.Marshal(mlConfig)
	if err != nil {
		return fmt.Errorf("error marshaling GlobalConfig: %w", err)
	}
	bf.WriteString("\t\"MoLingConfig\":\n")
	bf.WriteString(fmt.Sprintf("\t%s,\n", mlConfigJSON))
	first := true
	for srvName, nsv := range services.ServiceList() {
		// 获取服务对应的配置
		cfg, ok := nowConfigJSON[string(srvName)].(map[string]any)

		srv, err := nsv(ctx)
		if err != nil {
			return err
		}
		// srv Loadconfig
		if ok {
			err = srv.LoadConfig(cfg)
			if err != nil {
				return fmt.Errorf("error loading config for service %s: %w", srv.Name(), err)
			}
		} else {
			logger.Debug().Str("service", string(srv.Name())).Msg("Service not found in config, using default config")
		}
		// srv Init
		err = srv.Init()
		if err != nil {
			return fmt.Errorf("error initializing service %s: %w", srv.Name(), err)
		}
		if !first {
			bf.WriteString(",\n")
		}
		bf.WriteString(fmt.Sprintf("\t\"%s\":\n", srv.Name()))
		bf.WriteString(fmt.Sprintf("\t%s\n", srv.Config()))
		first = false
	}
	bf.WriteString("}\n")
	// 解析原始 JSON 字符串
	var data any
	err = json.Unmarshal(bf.Bytes(), &data)
	if err != nil {
		return fmt.Errorf("error unmarshaling JSON: %w, payload:%s", err, bf.String())
	}

	// 格式化 JSON
	formattedJSON, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return fmt.Errorf("error marshaling JSON: %w", err)
	}

	// 如果不存在配置文件
	if !hasConfig {
		logger.Info().Msgf("Configuration file %s does not exist. Creating a new one.", configFilePath)
		err = os.WriteFile(configFilePath, formattedJSON, 0644)
		if err != nil {
			return fmt.Errorf("error writing configuration file: %w", err)
		}
		logger.Info().Msgf("Configuration file %s created successfully.", configFilePath)
	}
	logger.Info().Str("config", configFilePath).Msg("Current loaded configuration file path")
	logger.Info().Msg("You can modify the configuration file to change the settings.")
	if !initial {
		logger.Info().Msgf("Configuration details: \n%s\n", formattedJSON)
	}
	return nil
}

func init() {
	configCmd.PersistentFlags().BoolVar(&initial, "init", false, fmt.Sprintf("Save configuration to %s", filepath.Join(mlConfig.BasePath, mlConfig.ConfigFile)))
	rootCmd.AddCommand(configCmd)
}


================================================
FILE: cli/cmd/perrun.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package cmd

import (
	"path/filepath"

	"github.com/spf13/cobra"

	"github.com/gojue/moling/pkg/utils"
)

// mlsCommandPreFunc is a pre-run function for the MoLing command.
func mlsCommandPreFunc(cmd *cobra.Command, args []string) error {
	err := utils.CreateDirectory(mlConfig.BasePath)
	if err != nil {
		return err
	}
	for _, dirName := range mlDirectories {
		err = utils.CreateDirectory(filepath.Join(mlConfig.BasePath, dirName))
		if err != nil {
			return err
		}
	}
	return nil
}


================================================
FILE: cli/cmd/root.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package cmd

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/signal"
	"os/user"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/rs/zerolog"
	"github.com/spf13/cobra"

	"github.com/gojue/moling/cli/cobrautl"
	"github.com/gojue/moling/pkg/comm"
	"github.com/gojue/moling/pkg/config"
	"github.com/gojue/moling/pkg/server"
	"github.com/gojue/moling/pkg/services"
	"github.com/gojue/moling/pkg/services/abstract"
	"github.com/gojue/moling/pkg/utils"
)

const (
	CliName            = "moling"
	CliNameZh          = "魔灵"
	MCPServerName      = "MoLing MCP Server"
	CliDescription     = "MoLing is a computer-use and browser-use based MCP server. It is a locally deployed, dependency-free office AI assistant."
	CliDescriptionZh   = "MoLing(魔灵)是一款基于computer-use和浏browser-use的 MCP 服务器,它是一个本地部署、无依赖的办公 AI 助手。"
	CliHomepage        = "https://gojue.cc/moling"
	CliAuthor          = "CFC4N <cfc4ncs@gmail.com>"
	CliGithubRepo      = "https://github.com/gojue/moling"
	CliDescriptionLong = `
MoLing is a computer-based 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.

Requiring no installation of any dependencies, MoLing can be run directly and is compatible with multiple operating systems, including Windows, Linux, and macOS. This eliminates the hassle of dealing with environment conflicts involving Node.js, Python, and other development environments.

Usage:
  moling
  moling -l 127.0.0.1:6789
  moling -h
  moling client -i
  moling config 
`
	CliDescriptionLongZh = `MoLing(魔灵)是一个computer-use的MCP Server,基于操作系统API实现了系统交互,可以实现文件系统的读写、合并、统计、聚合等操作,也可以执行系统命令操作。是一个无需任何依赖的本地办公自动化助手。
没有任何安装依赖,直接运行,兼容Windows、Linux、macOS等操作系统。再也不用苦恼NodeJS、Python等环境冲突等问题。

Usage:
  moling
  moling -l 127.0.0.1:29118
  moling -h
  moling client -i
  moling config 
`
)

const (
	MLConfigName = "config.json"     // config file name of MoLing Server
	MLRootPath   = ".moling"         // config file name of MoLing Server
	MLPidName    = "moling.pid"      // pid file name
	LogFileName  = "moling.log"      //	log file name
	MaxLogSize   = 1024 * 1024 * 512 // 512MB
)

var (
	GitVersion = "unknown_arm64_v0.0.0_2025-03-22 20:08"
	mlConfig   = &config.MoLingConfig{
		Version:    GitVersion,
		ConfigFile: filepath.Join("config", MLConfigName),
		BasePath:   filepath.Join(os.TempDir(), MLRootPath), // will set in mlsCommandPreFunc
	}

	// mlDirectories is a list of directories to be created in the base path
	mlDirectories = []string{
		"logs",    // log file
		"config",  // config file
		"browser", // browser cache
		"data",    // data
		"cache",
	}
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:        CliName,
	Short:      CliDescription,
	SuggestFor: []string{"molin", "moli", "mling"},

	Long: CliDescriptionLong,
	// Uncomment the following line if your bare application
	// has an action associated with it:
	RunE:              mlsCommandFunc,
	PersistentPreRunE: mlsCommandPreFunc,
}

func usageFunc(c *cobra.Command) error {
	return cobrautl.UsageFunc(c, GitVersion)
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
	rootCmd.SetUsageFunc(usageFunc)
	rootCmd.SetHelpTemplate(`{{.UsageString}}`)
	rootCmd.CompletionOptions.DisableDefaultCmd = true
	rootCmd.Version = GitVersion
	rootCmd.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "version:\t%s" .Version}}
`)
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

func init() {
	// set default config file path
	currentUser, err := user.Current()
	if err == nil {
		mlConfig.BasePath = filepath.Join(currentUser.HomeDir, MLRootPath)
	}

	cobra.EnablePrefixMatching = true
	// Cobra also supports local flags, which will only run
	// when this action is called directly.
	rootCmd.PersistentFlags().StringVar(&mlConfig.BasePath, "base_path", mlConfig.BasePath, "MoLing Base Data Path, automatically set by the system, cannot be changed, display only.")
	rootCmd.PersistentFlags().BoolVarP(&mlConfig.Debug, "debug", "d", false, "Debug mode, default is false.")
	rootCmd.PersistentFlags().StringVarP(&mlConfig.ListenAddr, "listen_addr", "l", "", "listen address for SSE mode. default:'', not listen, used STDIO mode.")
	rootCmd.PersistentFlags().StringVarP(&mlConfig.AuthToken, "token", "t", "", "auth token for SSE mode. Auto-generated if empty. Clients must supply it as ?token=<token> or Authorization: Bearer <token>.")
	rootCmd.PersistentFlags().StringVarP(&mlConfig.Module, "module", "m", "all", "module to load, default: all; others: Browser,FileSystem,Command, etc. Multiple modules are separated by commas")
	rootCmd.SilenceUsage = true
}

// initLogger init logger
func initLogger(mlDataPath string) zerolog.Logger {
	var logger zerolog.Logger
	var err error
	logFile := filepath.Join(mlDataPath, "logs", LogFileName)
	zerolog.SetGlobalLevel(zerolog.InfoLevel)
	if mlConfig.Debug {
		zerolog.SetGlobalLevel(zerolog.DebugLevel)
	}

	// 初始化 RotateWriter
	rw, err := utils.NewRotateWriter(logFile, MaxLogSize) // 512MB 阈值
	if err != nil {
		panic(fmt.Sprintf("failed to open log file %s: %s", logFile, err.Error()))
	}
	logger = zerolog.New(rw).With().Timestamp().Logger()
	logger.Info().Uint32("MaxLogSize", MaxLogSize).Msgf("Log files are automatically rotated when they exceed the size threshold, and saved to %s.1 and %s.2 respectively", LogFileName, LogFileName)
	return logger
}

func mlsCommandFunc(command *cobra.Command, args []string) error {
	loger := initLogger(mlConfig.BasePath)
	mlConfig.SetLogger(loger)
	var err error
	var nowConfig []byte
	var nowConfigJSON map[string]any

	// 增加实例重复运行检测
	pidFilePath := filepath.Join(mlConfig.BasePath, MLPidName)
	loger.Info().Str("pid", pidFilePath).Msg("Starting MoLing MCP Server...")
	err = utils.CreatePIDFile(pidFilePath)
	if err != nil {
		return err
	}

	// 当前配置文件检测
	loger.Info().Str("ServerName", MCPServerName).Str("version", GitVersion).Msg("start")
	configFilePath := filepath.Join(mlConfig.BasePath, mlConfig.ConfigFile)
	if nowConfig, err = os.ReadFile(configFilePath); err == nil {
		err = json.Unmarshal(nowConfig, &nowConfigJSON)
		if err != nil {
			return fmt.Errorf("error unmarshaling JSON: %w, config file:%s", err, configFilePath)
		}
	}
	loger.Info().Str("config_file", configFilePath).Msg("load config file")
	ctx := context.WithValue(context.Background(), comm.MoLingConfigKey, mlConfig)
	ctx = context.WithValue(ctx, comm.MoLingLoggerKey, loger)
	ctxNew, cancelFunc := context.WithCancel(ctx)

	var modules []string
	if mlConfig.Module != "all" {
		modules = strings.Split(mlConfig.Module, ",")
	}
	var srvs []abstract.Service
	var closers = make(map[string]func() error)
	for srvName, nsv := range services.ServiceList() {
		if len(modules) > 0 {
			if !utils.StringInSlice(string(srvName), modules) {
				loger.Debug().Str("moduleName", string(srvName)).Msgf("module %s not in %v, skip", string(srvName), modules)
				continue
			}
			loger.Debug().Str("moduleName", string(srvName)).Msgf("starting %s service", srvName)
		}
		cfg, ok := nowConfigJSON[string(srvName)].(map[string]any)
		srv, err := nsv(ctxNew)
		if err != nil {
			loger.Error().Err(err).Msgf("failed to create service %s", srv.Name())
			break
		}
		if ok {
			err = srv.LoadConfig(cfg)
			if err != nil {
				loger.Error().Err(err).Msgf("failed to load config for service %s", srv.Name())
				break
			}
		}
		err = srv.Init()
		if err != nil {
			loger.Error().Err(err).Msgf("failed to init service %s", srv.Name())
			break
		}
		srvs = append(srvs, srv)
		closers[string(srv.Name())] = srv.Close
	}
	// MCPServer
	srv, err := server.NewMoLingServer(ctxNew, srvs, *mlConfig)
	if err != nil {
		loger.Error().Err(err).Msg("failed to create server")
		cancelFunc()
		return err
	}

	go func() {
		err = srv.Serve()
		if err != nil {
			loger.Error().Err(err).Msg("failed to start server")
			cancelFunc()
			return
		}
	}()

	// 创建一个信号通道
	sigChan := make(chan os.Signal, 2)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	// 创建一个 goroutine 来判断父进程是否退出
	// Claude Desktop 0.9.2 退出时,没有向MCP Server发送 SIGTERM信号,导致MCP 不能正常退出。
	// fix https://github.com/gojue/moling/issues/32
	go func() {
		ppid := os.Getppid()
		for {
			time.Sleep(1 * time.Second)
			newPpid := os.Getppid()
			if newPpid == 1 {
				loger.Info().Msgf("parent process changed,origin PPid:%d, New PPid:%d", ppid, newPpid)
				loger.Warn().Msg("parent process exited")
				sigChan <- syscall.SIGTERM
				break
			}
		}
	}()

	// 等待信号
	_ = <-sigChan
	loger.Info().Msg("Received signal, shutting down...")

	// close all services
	// close all services
	var wg sync.WaitGroup
	done := make(chan struct{})

	// 在goroutine中等待所有服务关闭
	go func() {
		for srvName, closer := range closers {
			wg.Add(1)
			go func(name string, closeFn func() error) {
				defer wg.Done()
				err := closeFn()
				if err != nil {
					loger.Error().Err(err).Msgf("failed to close service %s", name)
				} else {
					loger.Info().Msgf("service %s closed", name)
				}
			}(srvName, closer)
		}

		// 等待所有服务关闭
		wg.Wait()
		close(done)
	}()

	// 使用select等待完成或超时
	select {
	case <-time.After(5 * time.Second):
		cancelFunc()
		loger.Info().Msg("timeout, all services closed forcefully")
	case <-done:
		cancelFunc()
		loger.Info().Msg("all services closed")
	}
	err = utils.RemovePIDFile(pidFilePath)
	if err != nil {
		loger.Error().Err(err).Msgf("failed to remove pid file %s", pidFilePath)
		return err
	}
	loger.Info().Msgf("removed pid file %s", pidFilePath)
	loger.Info().Msg(" Bye!")
	return nil
}


================================================
FILE: cli/cmd/utils.go
================================================
package cmd


================================================
FILE: cli/cobrautl/help.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package cobrautl

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
	"text/tabwriter"
	"text/template"

	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
)

var (
	commandUsageTemplate *template.Template
	templFuncs           = template.FuncMap{
		"descToLines": func(s string) []string {
			// trim leading/trailing whitespace and split into slice of lines
			return strings.Split(strings.Trim(s, "\n\t "), "\n")
		},
		"cmdName": func(cmd *cobra.Command, startCmd *cobra.Command) string {
			parts := []string{cmd.Name()}
			for cmd.HasParent() && cmd.Parent().Name() != startCmd.Name() {
				cmd = cmd.Parent()
				parts = append([]string{cmd.Name()}, parts...)
			}
			return strings.Join(parts, " ")
		},
	}
)

func init() {
	commandUsage := `
{{ $cmd := .Cmd }}\
{{ $cmdname := cmdName .Cmd .Cmd.Root }}\
NAME:
{{ if not .Cmd.HasParent }}\
{{printf "\t%s - %s" .Cmd.Name .Cmd.Short}}
{{else}}\
{{printf "\t%s - %s" $cmdname .Cmd.Short}}
{{end}}\

USAGE:
{{printf "\t%s" .Cmd.UseLine}}
{{ if not .Cmd.HasParent }}\

VERSION:
{{printf "\t%s" .Version}}
{{end}}\
{{if .Cmd.HasSubCommands}}\

COMMANDS:
{{range .SubCommands}}\
{{ $cmdname := cmdName . $cmd }}\
{{ if .Runnable }}\
{{printf "\t%s\t%s" $cmdname .Short}}
{{end}}\
{{end}}\
{{end}}\
{{ if .Cmd.Long }}\

DESCRIPTION:
{{range $line := descToLines .Cmd.Long}}{{printf "\t%s" $line}}
{{end}}\
{{end}}\
{{if .Cmd.HasLocalFlags}}\

OPTIONS:
{{.LocalFlags}}\
{{end}}\
{{if .Cmd.HasInheritedFlags}}\

GLOBAL OPTIONS:
{{.GlobalFlags}}\
{{end}}
`[1:]

	commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(strings.Replace(commandUsage, "\\\n", "", -1)))
}

func molingFlagUsages(flagSet *pflag.FlagSet) string {
	x := new(bytes.Buffer)

	flagSet.VisitAll(func(flag *pflag.Flag) {
		if len(flag.Deprecated) > 0 {
			return
		}
		var format string
		if len(flag.Shorthand) > 0 {
			format = "  -%s, --%s"
		} else {
			format = "   %s   --%s"
		}
		if len(flag.NoOptDefVal) > 0 {
			format = format + "["
		}
		if flag.Value.Type() == "string" {
			// put quotes on the value
			format = format + "=%q"
		} else {
			format = format + "=%s"
		}
		if len(flag.NoOptDefVal) > 0 {
			format = format + "]"
		}
		format = format + "\t%s\n"
		shorthand := flag.Shorthand
		fmt.Fprintf(x, format, shorthand, flag.Name, flag.DefValue, flag.Usage)
	})

	return x.String()
}

func getSubCommands(cmd *cobra.Command) []*cobra.Command {
	var subCommands []*cobra.Command
	for _, subCmd := range cmd.Commands() {
		subCommands = append(subCommands, subCmd)
		subCommands = append(subCommands, getSubCommands(subCmd)...)
	}
	return subCommands
}

func UsageFunc(cmd *cobra.Command, version string) error {
	subCommands := getSubCommands(cmd)
	tabOut := getTabOutWithWriter(os.Stdout)
	err := commandUsageTemplate.Execute(tabOut, struct {
		Cmd         *cobra.Command
		LocalFlags  string
		GlobalFlags string
		SubCommands []*cobra.Command
		Version     string
	}{
		cmd,
		molingFlagUsages(cmd.LocalFlags()),
		molingFlagUsages(cmd.InheritedFlags()),
		subCommands,
		version,
	})
	if err != nil {
		return err
	}
	err = tabOut.Flush()
	return err
}

func getTabOutWithWriter(writer io.Writer) *tabwriter.Writer {
	aTabOut := new(tabwriter.Writer)
	aTabOut.Init(writer, 0, 8, 1, '\t', 0)
	return aTabOut
}


================================================
FILE: cli/main.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package cli

import (
	"github.com/gojue/moling/cli/cmd"
)

func Start() {
	cmd.Execute()
}


================================================
FILE: client/client.go
================================================
/*
 *
 *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *  Repository: https://github.com/gojue/moling
 *
 */

package client

import (
	"encoding/json"
	"errors"
	"os"

	"github.com/rs/zerolog"
)

var (
	// ClineConfigPath is the path to the Cline config file.
	clientLists = make(map[string]string, 3)
)

const MCPServersKey = "mcpServers"

// MCPServerConfig represents the configuration for the MCP Client.
type MCPServerConfig struct {
	Description string   `json:"description"`       // Description of the MCP Server
	IsActive    bool     `json:"isActive"`          // Is the MCP Server active
	Command     string   `json:"command,omitempty"` // Command to start the MCP Server, STDIO mode only
	Args        []string `json:"args,omitempty"`    // Arguments to pass to the command, STDIO mode only
	BaseURL     string   `json:"baseUrl,omitempty"` // Base URL of the MCP Server, SSE mode only
	TimeOut     uint16   `json:"timeout,omitempty"` // Timeout for the MCP Server, default is 300 seconds
	ServerName  string
}

// NewMCPServerConfig creates a new MCPServerConfig instance.
func NewMCPServerConfig(description string, command string, srvName string) MCPServerConfig {
	return MCPServerConfig{
		Description: description,
		IsActive:    true,
		Command:     command,
		Args:        []string{"-m", "Browser"},
		BaseURL:     "",
		ServerName:  srvName,
		TimeOut:     300,
	}
}

// Manager manages the configuration of different clients.
type Manager struct {
	logger    zerolog.Logger
	clients   map[string]string
	mcpConfig MCPServerConfig
}

// NewManager creates a new ClientManager instance.
func NewManager(lger zerolog.Logger, mcpConfig MCPServerConfig) (cm *Manager) {
	cm = &Manager{
		clients:   make(map[string]string, 3),
		logger:    lger,
		mcpConfig: mcpConfig,
	}
	cm.clients = clientLists
	return cm
}

// ListClient lists all the clients and checks if they exist.
func (c *Manager) ListClient() {
	for name, path := range c.clients {
		c.logger.Debug().Msgf("Client %s: %s", name, path)
		if !c.checkExist(path) {
			// path not exists
			c.logger.Info().Str("Client Name", name).Bool("exist", false).Msg("Client is not exist")
		} else {
			c.logger.Info().Str("Client Name", name).Bool("exist", true).Msg("Client is exist")
		}
	}
	return
}

// SetupConfig sets up the configuration for the clients.
func (c *Manager) SetupConfig() {
	for name, path := range c.clients {
		c.logger.Debug().Msgf("Client %s: %s", name, path)
		if !c.checkExist(path) {
			continue
		}
		// read config file
		file, err := os.ReadFile(path)
		if err != nil {
			c.logger.Error().Str("Client Name", name).Msgf("Failed to open config file %s: %s", path, err)
			continue
		}
		c.logger.Debug().Str("Client Name", name).Str("config", string(file)).Send()
		b, err := c.appendConfig(c.mcpConfig.ServerName, file)
		if err != nil {
			c.logger.Error().Str("Client Name", name).Msgf("Failed to append config file %s: %s", path, err)
			continue
		}
		c.logger.Debug().Str("Client Name", name).Str("newConfig", string(b)).Send()
		// write config file
		err = os.WriteFile(path, b, 0644)
		if err != nil {
			c.logger.Error().Str("Client Name", name).Msgf("Failed to write config file %s: %s", path, err)
			continue
		}
		c.logger.Info().Str("Client Name", name).Msgf("Successfully added config to %s", path)
	}
}

// appendConfig appends the mlMCPConfig to the client config.
func (c *Manager) appendConfig(name string, payload []byte) ([]byte, error) {
	var err error
	var jsonMap map[string]any
	var jsonBytes []byte
	err = json.Unmarshal(payload, &jsonMap)
	if err != nil {
		return nil, err
	}
	jsonMcpServer, ok := jsonMap[MCPServersKey].(map[string]any)
	if !ok {
		return nil, errors.New("MCPServersKey not found in JSON")
	}
	jsonMcpServer[name] = c.mcpConfig
	jsonMap[MCPServersKey] = jsonMcpServer
	jsonBytes, err = json.MarshalIndent(jsonMap, "", "  ")
	if err != nil {
		return nil, err
	}
	return jsonBytes, nil
}

// checkExist checks if the file at the given path exists.
func (c *Manager) checkExist(path string) bool {
	_, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			c.logger.Debug().Msgf("Client config file %s does not exist", path)
			return false
		}
		c.logger.Info().Msgf("check file failed, error:%s", err.Error())
		return false
	}
	return true
}


================================================
FILE: client/client_config.go
================================================
//go:build !windows

/*
 *
 *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *  Repository: https://github.com/gojue/moling
 *
 */

package client

import (
	"os"
	"path/filepath"
)

func init() {
	clientLists["VSCode Cline"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
	clientLists["Trae CN Cline"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae CN", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
	clientLists["Trae Cline"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
	clientLists["Trae"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae", "User", "mcp.json")
	clientLists["Trae CN"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae CN", "User", "mcp.json")
	clientLists["VSCode Roo"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
	clientLists["Trae CN Roo"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae CN", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
	clientLists["Trae Roo"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Trae", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
	clientLists["Claude"] = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Claude", "claude_desktop_config.json")
	clientLists["Cursor"] = filepath.Join(os.Getenv("HOME"), ".cursor", "mcp.json")
}


================================================
FILE: client/client_config_windows.go
================================================
/*
 *
 *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *  Repository: https://github.com/gojue/moling
 *
 */

package client

import (
	"os"
	"path/filepath"
)

func init() {
	clientLists["VSCODE Cline"] = filepath.Join(os.Getenv("APPDATA"), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
	clientLists["Trae CN Cline"] = filepath.Join(os.Getenv("APPDATA"), "Trae CN", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
	clientLists["Trae Cline"] = filepath.Join(os.Getenv("APPDATA"), "Trae", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
	clientLists["Trae"] = filepath.Join(os.Getenv("APPDATA"), "Trae", "User", "mcp.json")
	clientLists["Trae CN"] = filepath.Join(os.Getenv("APPDATA"), "Trae CN", "User", "mcp.json")
	clientLists["VSCODE Roo Code"] = filepath.Join(os.Getenv("APPDATA"), "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
	clientLists["Trae CN Roo"] = filepath.Join(os.Getenv("APPDATA"), "Trae CN", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "mcp_settings.json")
	clientLists["Trae Roo"] = filepath.Join(os.Getenv("APPDATA"), "Trae", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "mcp_settings.json")
	clientLists["Claude"] = filepath.Join(os.Getenv("APPDATA"), "Claude", "claude_desktop_config.json")
	clientLists["Cursor"] = filepath.Join(os.Getenv("APPDATA"), "Cursor", "mcp.json")
}


================================================
FILE: client/client_test.go
================================================
/*
 *
 *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *  Repository: https://github.com/gojue/moling
 *
 */

package client

import (
	"os"
	"testing"

	"github.com/rs/zerolog"
)

func TestClientManager_ListClient(t *testing.T) {
	logger := zerolog.New(os.Stdout)
	mcpConfig := NewMCPServerConfig("MoLing UnitTest Description", "moling_test", "MoLing MCP Server")
	cm := NewManager(logger, mcpConfig)
	// Mock client list
	clientLists["TestClient"] = "/path/to/nonexistent/file"

	cm.ListClient()
	// Check logs or other side effects as needed
}

/*
	func TestClientManager_SetupConfig(t *testing.T) {
		logger := zerolog.New(os.Stdout)
		mcpConfig := NewMCPServerConfig("MoLing UnitTest Description", "moling_test", "MoLing MCP Server")
		cm := NewManager(logger, mcpConfig)

		// Mock client list
		clientLists["TestClient"] = "/path/to/nonexistent/file"

		cm.SetupConfig()
		// Check logs or other side effects as needed
	}

	func TestClientManager_appendConfig(t *testing.T) {
		logger := zerolog.New(os.Stdout)
		mcpConfig := NewMCPServerConfig("MoLing UnitTest Description", "moling_test", "MoLing MCP Server")
		cm := NewManager(logger, mcpConfig)

		// Mock payload
		payload := []byte(`{
	  "Cline": {
	    "description": "MoLing UnitTest Description",
	    "isActive": true,
	    "command": "moling_test"
	  },
	  "mcpServers": {
	    "testABC": {
	      "args": [
	        "--allow-dir",
	        "/tmp/,/Users/username/Downloads"
	      ],
	      "command": "npx",
	      "timeout": 300
	    }
	  }
	}`)

	result, err := cm.appendConfig("TestClient", payload)
	if err != nil {
		t.Fatalf("Expected no error, got %s", err.Error())
	}

	var resultMap map[string]any
	err = json.Unmarshal(result, &resultMap)
	if err != nil {
		t.Fatalf("Expected valid JSON, got error %s", err.Error())
	}

	if resultMap["existingKey"] != "existingValue" {
		t.Errorf("Expected existingKey to be existingValue, got %v", resultMap["existingKey"])
	}

}
*/

func TestClientManager_checkExist(t *testing.T) {
	logger := zerolog.New(os.Stdout)
	mcpConfig := NewMCPServerConfig("MoLing UnitTest Description", "moling_test", "MoLing MCP Server")
	cm := NewManager(logger, mcpConfig)

	// Test with a non-existent file
	exists := cm.checkExist("/path/to/nonexistent/file")
	if exists {
		t.Errorf("Expected file to not exist")
	}

	// Test with an existing file
	file, err := os.CreateTemp("", "testfile")
	if err != nil {
		t.Fatalf("Failed to create temp file: %s", err.Error())
	}
	defer func() {
		_ = os.Remove(file.Name())
	}()
	t.Logf("Created temp file: %s", file.Name())
	exists = cm.checkExist(file.Name())
	if !exists {
		t.Errorf("Expected file to exist")
	}
}


================================================
FILE: dist/.gitkeep
================================================


================================================
FILE: functions.mk
================================================
define allow-override
  $(if $(or $(findstring environment,$(origin $(1))),\
            $(findstring command line,$(origin $(1)))),,\
    $(eval $(1) = $(2)))
endef

# TARGET_OS , TARGET_ARCH
define gobuild
	CGO_ENABLED=0 \
	GOOS=$(1) GOARCH=$(2) \
	$(eval OUT_BIN_SUFFIX=$(if $(filter $(1),windows),.exe,)) \
	$(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)
	$(CMD_FILE) $(OUT_BIN)$(OUT_BIN_SUFFIX)
endef



================================================
FILE: go.mod
================================================
module github.com/gojue/moling

go 1.24.1

require (
	github.com/chromedp/cdproto v0.0.0-20250518235601-40b4c35ec9fe
	github.com/chromedp/chromedp v0.13.6
	github.com/mark3labs/mcp-go v0.29.0
	github.com/rs/zerolog v1.34.0
	github.com/spf13/cobra v1.9.1
	github.com/spf13/pflag v1.0.6
)

require (
	github.com/chromedp/sysutil v1.1.0 // indirect
	github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect
	github.com/gobwas/httphead v0.1.0 // indirect
	github.com/gobwas/pool v0.2.1 // indirect
	github.com/gobwas/ws v1.4.0 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/spf13/cast v1.8.0 // indirect
	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
	golang.org/x/sys v0.33.0 // indirect
)


================================================
FILE: go.sum
================================================
github.com/chromedp/cdproto v0.0.0-20250518235601-40b4c35ec9fe h1:roGYW+2lkWq2EdEOrSOxj8+L07gG1q6iF3xeKUHfcDQ=
github.com/chromedp/cdproto v0.0.0-20250518235601-40b4c35ec9fe/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.6 h1:xlNunMyzS5bu3r/QKrb3fzX6ow3WBQ6oao+J65PGZxk=
github.com/chromedp/chromedp v0.13.6/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk=
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/mark3labs/mcp-go v0.29.0 h1:sH1NBcumKskhxqYzhXfGc201D7P76TVXiT0fGVhabeI=
github.com/mark3labs/mcp-go v0.29.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: install/install.ps1
================================================
#!/usr/bin/env pwsh

Set-StrictMode -Version Latest

Write-Output "Welcome to MoLing MCP Server initialization script."
Write-Output "Home page: https://gojue.cc/moling"
Write-Output "Github: https://github.com/gojue/moling"

# Determine the OS and architecture
$OS = (Get-CimInstance Win32_OperatingSystem).Caption
$ARCH = (Get-CimInstance Win32_Processor).Architecture

switch ($ARCH) {
    9 { $ARCH = "amd64" }
    5 { $ARCH = "arm64" }
    default {
        Write-Error "Unsupported architecture: $ARCH"
        exit 1
    }
}

# Determine the download URL
$VERSION = "v0.0.1"
$BASE_URL = "https://github.com/gojue/moling/releases/download/$VERSION"
$FILE_NAME = "moling-$VERSION-windows-$ARCH.zip"
$DOWNLOAD_URL = "$BASE_URL/$FILE_NAME"

# Download the installation package
Write-Output "Downloading $DOWNLOAD_URL..."
Invoke-WebRequest -Uri $DOWNLOAD_URL -OutFile $FILE_NAME

# Extract the package
Write-Output "Extracting $FILE_NAME..."
Expand-Archive -Path $FILE_NAME -DestinationPath "moling"

# Move the binary to C:\Program Files
$destination = "C:\Program Files\moling"
if (-Not (Test-Path -Path $destination)) {
    New-Item -ItemType Directory -Path $destination
}
Move-Item -Path "moling\moling.exe" -Destination "$destination\moling.exe"

# Add to PATH
$env:Path += ";$destination"
[System.Environment]::SetEnvironmentVariable("Path", $env:Path, [System.EnvironmentVariableTarget]::Machine)

# MCP Client configuration
& "$destination\moling.exe" client --install

# Clean up
Remove-Item -Recurse -Force "moling"
Remove-Item -Force $FILE_NAME

Write-Output "MoLing has been installed successfully!"

================================================
FILE: install/install.sh
================================================
#!/bin/bash

set -e

echo "Welcome to MoLing MCP Server initialization script."
echo "Home page: https://gojue.cc/moling"
echo "Github: https://github.com/gojue/moling"

# Determine the OS and architecture
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)

case $ARCH in
  x86_64)
    ARCH="amd64"
    ;;
  arm64|aarch64)
    ARCH="arm64"
    ;;
  *)
    echo "Unsupported architecture: $ARCH"
    exit 1
    ;;
esac

# Determine the download URL
VERSION=$(curl -s https://api.github.com/repos/gojue/moling/releases/latest | grep 'tag_name' | cut -d\" -f4)
BASE_URL="https://github.com/gojue/moling/releases/download/${VERSION}"
FILE_NAME="moling-${VERSION}-${OS}-${ARCH}.tar.gz"

DOWNLOAD_URL="${BASE_URL}/${FILE_NAME}"

# Download the installation package
echo "Downloading ${DOWNLOAD_URL}..."
curl -LO "${DOWNLOAD_URL}"
echo "Download completed. filename: ${FILE_NAME}"
# Extract the package
tar -xzf "${FILE_NAME}"

# Move the binary to /usr/local/bin
mv moling /usr/local/bin/moling
chmod +x /usr/local/bin/moling

# Clean up
rm -rf moling "${FILE_NAME}"

# Check if the installation was successful
if command -v moling &> /dev/null; then
    echo "MoLing installation was successful!"
else
    echo "MoLing installation failed."
    exit 1
fi

# initialize the configuration
echo "Initializing MoLing configuration..."
moling config --init
echo "MoLing configuration initialized successfully!"

echo "setup MCP Server configuration into MCP Client"
moling client -i
echo "MCP Client configuration setup successfully!"

================================================
FILE: main.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import "github.com/gojue/moling/cli"

func main() {
	cli.Start()
}


================================================
FILE: pkg/comm/comm.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package comm

import (
	"context"
	"os"
	"path/filepath"

	"github.com/rs/zerolog"

	"github.com/gojue/moling/pkg/config"
)

type MoLingServerType string

type contextKey string

// MoLingConfigKey is a context key for storing the version of MoLing
const (
	MoLingConfigKey contextKey = "moling_config"
	MoLingLoggerKey contextKey = "moling_logger"
)

// InitTestEnv initializes the test environment by creating a temporary log file and setting up the logger.
func InitTestEnv() (zerolog.Logger, context.Context, error) {
	logFile := filepath.Join(os.TempDir(), "moling.log")
	zerolog.SetGlobalLevel(zerolog.DebugLevel)
	var logger zerolog.Logger
	f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
	if err != nil {
		return zerolog.Logger{}, nil, err
	}
	logger = zerolog.New(f).With().Timestamp().Logger()
	mlConfig := &config.MoLingConfig{
		ConfigFile: filepath.Join("config", "test_config.json"),
		BasePath:   os.TempDir(),
	}
	ctx := context.WithValue(context.Background(), MoLingConfigKey, mlConfig)
	ctx = context.WithValue(ctx, MoLingLoggerKey, logger)
	return logger, ctx, nil
}


================================================
FILE: pkg/comm/errors.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package comm

import "errors"

var (
	ErrConfigNotLoaded = errors.New("config not loaded, please call LoadConfig() first")
)


================================================
FILE: pkg/config/config.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package config

import (
	"github.com/rs/zerolog"
)

// Config is an interface that defines a method for checking configuration validity.
type Config interface {
	// Check validates the configuration and returns an error if the configuration is invalid.
	Check() error
}

// MoLingConfig is a struct that holds the configuration for the MoLing server.
type MoLingConfig struct {
	ConfigFile string `json:"config_file"` // The path to the configuration file.
	BasePath   string `json:"base_path"`   // The base path for the server, used for storing files. automatically created if not exists. eg: /Users/user1/.moling
	//AllowDir   []string `json:"allow_dir"`   // The directories that are allowed to be accessed by the server.
	Version    string `json:"version"`     // The version of the MoLing server.
	ListenAddr string `json:"listen_addr"` // The address to listen on for SSE mode.
	Debug      bool   `json:"debug"`       // Debug mode, if true, the server will run in debug mode.
	Module     string `json:"module"`      // The module to load, default: all
	Username   string // The username of the user running the server.
	HomeDir    string // The home directory of the user running the server. macOS: /Users/user1, Linux: /home/user1
	SystemInfo string // The system information of the user running the server. macOS: Darwin 15.3.3, Linux: Ubuntu 20.04.1 LTS

	// for MCP Server Config
	Description string // Description of the MCP Server, default: CliDescription
	Command     string //	Command to start the MCP Server, STDIO mode only,  default: CliName
	Args        string // Arguments to pass to the command, STDIO mode only, default: empty
	BaseURL     string // BaseURL , SSE mode only.
	ServerName  string // ServerName MCP ServerName, add to the MCP Client config
	AuthToken   string // AuthToken for SSE mode authentication. Auto-generated if empty.
	logger      zerolog.Logger
}

func (cfg *MoLingConfig) Check() error {
	panic("not implemented yet") // TODO: Implement Check
}

func (cfg *MoLingConfig) Logger() zerolog.Logger {
	return cfg.logger
}

func (cfg *MoLingConfig) SetLogger(logger zerolog.Logger) {
	cfg.logger = logger
}


================================================
FILE: pkg/config/config_test.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package config

import (
	"encoding/json"
	"os"
	"testing"

	"github.com/gojue/moling/pkg/utils"
)

// TestConfigLoad tests the loading of the configuration from a JSON file.
func TestConfigLoad(t *testing.T) {
	configFile := "config_test.json"
	cfg := &MoLingConfig{}
	cfg.ConfigFile = "config.json"
	cfg.BasePath = "/tmp/moling"
	cfg.Version = "1.0.0"
	cfg.ListenAddr = ":8080"
	cfg.Debug = true
	cfg.Username = "user1"
	cfg.HomeDir = "/Users/user1"
	cfg.SystemInfo = "Darwin 15.3.3"

	jsonData, err := os.ReadFile(configFile)
	if err != nil {
		t.Fatalf("failed to read config file: %s", err.Error())
	}
	var jsonMap map[string]any
	if err := json.Unmarshal(jsonData, &jsonMap); err != nil {
		t.Fatalf("Failed to unmarshal JSON: %s", err.Error())
	}
	mlConfig, ok := jsonMap["MoLingConfig"].(map[string]any)
	if !ok {
		t.Fatalf("failed to parse MoLingConfig from JSON")
	}
	if err := utils.MergeJSONToStruct(cfg, mlConfig); err != nil {
		t.Fatalf("failed to merge JSON to struct: %s", err.Error())
	}
	t.Logf("Config loaded, MoLing Config.BasePath: %s", cfg.BasePath)
	if cfg.BasePath != "/newpath/.moling" {
		t.Fatalf("expected BasePath to be '/newpath/.moling', got '%s'", cfg.BasePath)
	}
}


================================================
FILE: pkg/config/config_test.json
================================================
{
  "BrowserServer": {
  },
  "CommandServer": {
    "allowed_commands": [
      "ls",
      "cat",
      "echo"
    ]
  },
  "FilesystemServer": {
    "allowed_dirs": [
      "/tmp/.moling/data/"
    ],
    "cache_path": "/tmp/.moling/data"
  },
  "MoLingConfig": {
    "HomeDir": "",
    "SystemInfo": "",
    "Username": "",
    "base_path": "/newpath/.moling",
    "config_file": "config/config.json",
    "debug": false,
    "listen_addr": "",
    "version": "darwin-arm64-20250330084836-0077553"
  },
  "MoaServer": {}
}

================================================
FILE: pkg/server/server.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling
package server

import (
	"context"
	"crypto/rand"
	"crypto/subtle"
	"encoding/hex"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/mark3labs/mcp-go/server"
	"github.com/rs/zerolog"

	"github.com/gojue/moling/pkg/comm"
	"github.com/gojue/moling/pkg/config"
	"github.com/gojue/moling/pkg/services/abstract"
)

type MoLingServer struct {
	ctx        context.Context
	server     *server.MCPServer
	services   []abstract.Service
	logger     zerolog.Logger
	mlConfig   config.MoLingConfig
	listenAddr string // SSE mode listen address, if empty, use STDIO mode.
	authToken  string // Auth token for SSE mode. Required for all SSE requests.
}

func NewMoLingServer(ctx context.Context, srvs []abstract.Service, mlConfig config.MoLingConfig) (*MoLingServer, error) {
	mcpServer := server.NewMCPServer(
		mlConfig.ServerName,
		mlConfig.Version,
		server.WithResourceCapabilities(true, true),
		server.WithLogging(),
		server.WithPromptCapabilities(true),
	)

	// Resolve auth token for SSE mode.  16 random bytes encoded as 32 hex chars.
	authToken := mlConfig.AuthToken
	if mlConfig.ListenAddr != "" && authToken == "" {
		tokenBytes := make([]byte, 16)
		if _, err := rand.Read(tokenBytes); err != nil {
			return nil, fmt.Errorf("failed to generate SSE auth token: %w", err)
		}
		authToken = hex.EncodeToString(tokenBytes)
	}

	// Set the context for the server
	ms := &MoLingServer{
		ctx:        ctx,
		server:     mcpServer,
		services:   srvs,
		listenAddr: mlConfig.ListenAddr,
		logger:     ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger),
		mlConfig:   mlConfig,
		authToken:  authToken,
	}
	err := ms.init()
	return ms, err
}

func (m *MoLingServer) init() error {
	var err error
	for _, srv := range m.services {
		m.logger.Debug().Str("serviceName", string(srv.Name())).Msg("Loading service")
		err = m.loadService(srv)
		if err != nil {
			m.logger.Info().Err(err).Str("serviceName", string(srv.Name())).Msg("Failed to load service")
		}
	}
	return err
}

func (m *MoLingServer) loadService(srv abstract.Service) error {

	// Add resources
	for r, rhf := range srv.Resources() {
		m.server.AddResource(r, rhf)
	}

	// Add Resource Templates
	for rt, rthf := range srv.ResourceTemplates() {
		m.server.AddResourceTemplate(rt, rthf)
	}

	// Add Tools
	m.server.AddTools(srv.Tools()...)

	// Add Notification Handlers
	for n, nhf := range srv.NotificationHandlers() {
		m.server.AddNotificationHandler(n, nhf)
	}

	// Add Prompts
	for _, pe := range srv.Prompts() {
		// Add Prompt
		m.server.AddPrompt(pe.Prompt(), pe.Handler())
	}
	return nil
}

// requireJSONContentType is a middleware that rejects POST requests whose
// Content-Type is not application/json.  Browsers treat text/plain,
// application/x-www-form-urlencoded, and multipart/form-data as "simple"
// requests, meaning no CORS preflight is sent.  Enforcing application/json
// here ensures that cross-origin requests always go through the preflight
// check and cannot bypass CORS protection.
func requireJSONContentType(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodPost {
			ct := r.Header.Get("Content-Type")
			// Strip optional parameters (e.g. "; charset=utf-8") before comparing.
			mediaType := strings.ToLower(strings.TrimSpace(strings.SplitN(ct, ";", 2)[0]))
			if mediaType != "application/json" {
				http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType)
				return
			}
		}
		next.ServeHTTP(w, r)
	})
}

// corsRemoverResponseWriter wraps http.ResponseWriter to strip the wildcard
// Access-Control-Allow-Origin header hardcoded by the upstream mcp-go library,
// preventing cross-origin browser access to the SSE endpoint.
type corsRemoverResponseWriter struct {
	http.ResponseWriter
	flusher http.Flusher
	cleaned bool
}

func newCORSRemoverResponseWriter(w http.ResponseWriter) *corsRemoverResponseWriter {
	flusher, _ := w.(http.Flusher)
	return &corsRemoverResponseWriter{
		ResponseWriter: w,
		flusher:        flusher,
	}
}

func (w *corsRemoverResponseWriter) cleanCORSHeader() {
	if !w.cleaned {
		w.ResponseWriter.Header().Del("Access-Control-Allow-Origin")
		w.cleaned = true
	}
}

func (w *corsRemoverResponseWriter) WriteHeader(code int) {
	w.cleanCORSHeader()
	w.ResponseWriter.WriteHeader(code)
}

func (w *corsRemoverResponseWriter) Write(b []byte) (int, error) {
	w.cleanCORSHeader()
	return w.ResponseWriter.Write(b)
}

func (w *corsRemoverResponseWriter) Flush() {
	if w.flusher != nil {
		w.flusher.Flush()
	}
}

// sseSecurityMiddleware enforces token-based authentication and removes the
// wildcard CORS header from all responses.  Clients must supply the token either
// as an Authorization: Bearer <token> header or as a ?token=<token> query
// parameter.
func sseSecurityMiddleware(token string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Accept token from Authorization header (Bearer scheme only) or query parameter.
		// Use constant-time comparison to prevent timing attacks.
		var bearerToken string
		if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") {
			bearerToken = authHeader[len("Bearer "):]
		}
		queryToken := r.URL.Query().Get("token")
		tokenBytes := []byte(token)
		validBearer := subtle.ConstantTimeCompare([]byte(bearerToken), tokenBytes) == 1
		validQuery := subtle.ConstantTimeCompare([]byte(queryToken), tokenBytes) == 1
		if !validBearer && !validQuery {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(newCORSRemoverResponseWriter(w), r)
	})
}

func (m *MoLingServer) Serve() error {
	mLogger := log.New(m.logger, m.mlConfig.ServerName, 0)
	if m.listenAddr != "" {
		ltnAddr := fmt.Sprintf("http://%s", strings.TrimPrefix(m.listenAddr, "http://"))
		consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
		multi := zerolog.MultiLevelWriter(consoleWriter, m.logger)
		m.logger = zerolog.New(multi).With().Timestamp().Logger()
		m.logger.Info().Str("listenAddr", m.listenAddr).Str("BaseURL", ltnAddr).Msg("Starting SSE server")
		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)
		// Print auth token to stdout only — avoid persisting credentials to log file.
		stdoutLogger := zerolog.New(consoleWriter).With().Timestamp().Logger()
		stdoutLogger.Warn().Msgf("SSE auth token: %s", m.authToken)
		stdoutLogger.Warn().Msgf("SSE server URL with token: %s/sse?token=%s", ltnAddr, m.authToken)
		httpSrv := &http.Server{Addr: m.listenAddr}
		sseServer := server.NewSSEServer(m.server, server.WithBaseURL(ltnAddr), server.WithHTTPServer(httpSrv))
		httpSrv.Handler = sseSecurityMiddleware(m.authToken, requireJSONContentType(sseServer))

		return sseServer.Start(m.listenAddr)
	}
	m.logger.Info().Msg("Starting STDIO server")
	return server.ServeStdio(m.server, server.WithErrorLogger(mLogger))
}


================================================
FILE: pkg/server/server_test.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package server

import (
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/gojue/moling/pkg/comm"
	"github.com/gojue/moling/pkg/config"
	"github.com/gojue/moling/pkg/services/abstract"
	"github.com/gojue/moling/pkg/services/filesystem"
	"github.com/gojue/moling/pkg/utils"
)

func TestNewMLServer(t *testing.T) {
	// Create a new MoLingConfig
	mlConfig := config.MoLingConfig{
		BasePath: filepath.Join(os.TempDir(), "moling_test"),
	}
	mlDirectories := []string{
		"logs",    // log file
		"config",  // config file
		"browser", // browser cache
		"data",    // data
		"cache",
	}
	err := utils.CreateDirectory(mlConfig.BasePath)
	if err != nil {
		t.Errorf("Failed to create base directory: %s", err.Error())
	}
	for _, dirName := range mlDirectories {
		err = utils.CreateDirectory(filepath.Join(mlConfig.BasePath, dirName))
		if err != nil {
			t.Errorf("Failed to create directory %s: %s", dirName, err.Error())
		}
	}
	logger, ctx, err := comm.InitTestEnv()
	if err != nil {
		t.Fatalf("Failed to initialize test environment: %s", err.Error())
	}
	logger.Info().Msg("TestBrowserServer")
	mlConfig.SetLogger(logger)

	// Create a new server with the filesystem service
	fs, err := filesystem.NewFilesystemServer(ctx)
	if err != nil {
		t.Errorf("Failed to create filesystem server: %s", err.Error())
	}
	err = fs.Init()
	if err != nil {
		t.Errorf("Failed to initialize filesystem server: %s", err.Error())
	}
	srvs := []abstract.Service{
		fs,
	}
	srv, err := NewMoLingServer(ctx, srvs, mlConfig)
	if err != nil {
		t.Errorf("Failed to create server: %s", err.Error())
	}
	err = srv.Serve()
	if err != nil {
		t.Errorf("Failed to start server: %s", err.Error())
	}
	t.Logf("Server started successfully: %v", srv)
}

// TestSSESecurityMiddleware verifies that sseSecurityMiddleware enforces token
// authentication and that corsRemoverResponseWriter strips the wildcard CORS header.
func TestSSESecurityMiddleware(t *testing.T) {
	const token = "test-secret-token"

	// A stub upstream handler that sets CORS wildcard and writes a body.
	stub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})

	handler := sseSecurityMiddleware(token, stub)

	t.Run("no token returns 401", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/sse", nil)
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if rr.Code != http.StatusUnauthorized {
			t.Errorf("expected 401, got %d", rr.Code)
		}
	})

	t.Run("wrong token returns 401", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/sse?token=wrong", nil)
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if rr.Code != http.StatusUnauthorized {
			t.Errorf("expected 401, got %d", rr.Code)
		}
	})

	t.Run("raw token in Authorization header without Bearer scheme returns 401", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/sse", nil)
		req.Header.Set("Authorization", token) // missing "Bearer " prefix
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if rr.Code != http.StatusUnauthorized {
			t.Errorf("expected 401, got %d", rr.Code)
		}
	})

	t.Run("valid query param token passes and removes CORS header", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/sse?token="+token, nil)
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Errorf("expected 200, got %d", rr.Code)
		}
		if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "" {
			t.Errorf("expected CORS header to be removed, got %q", got)
		}
	})

	t.Run("valid Bearer token passes and removes CORS header", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/sse", nil)
		req.Header.Set("Authorization", "Bearer "+token)
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Errorf("expected 200, got %d", rr.Code)
		}
		if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "" {
			t.Errorf("expected CORS header to be removed, got %q", got)
		}
	})
}

// TestRequireJSONContentType verifies that the middleware blocks POST requests
// with non-application/json Content-Types (which browsers treat as "simple
// requests" and therefore never trigger a CORS preflight).
func TestRequireJSONContentType(t *testing.T) {
	// A simple downstream handler that always returns 200.
	ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	handler := requireJSONContentType(ok)

	tests := []struct {
		name           string
		method         string
		contentType    string
		expectedStatus int
	}{
		// Non-POST requests must pass through regardless of Content-Type.
		{"GET no CT", http.MethodGet, "", http.StatusOK},
		{"GET text/plain", http.MethodGet, "text/plain", http.StatusOK},
		// POST with application/json (with and without charset param) must pass.
		{"POST application/json", http.MethodPost, "application/json", http.StatusOK},
		{"POST application/json; charset=utf-8", http.MethodPost, "application/json; charset=utf-8", http.StatusOK},
		// POST with "simple" Content-Types that bypass CORS preflight must be rejected.
		{"POST text/plain", http.MethodPost, "text/plain", http.StatusUnsupportedMediaType},
		{"POST application/x-www-form-urlencoded", http.MethodPost, "application/x-www-form-urlencoded", http.StatusUnsupportedMediaType},
		{"POST multipart/form-data", http.MethodPost, "multipart/form-data", http.StatusUnsupportedMediaType},
		// POST with empty or missing Content-Type must also be rejected.
		{"POST no CT", http.MethodPost, "", http.StatusUnsupportedMediaType},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(tc.method, "/message", nil)
			if tc.contentType != "" {
				req.Header.Set("Content-Type", tc.contentType)
			}
			rr := httptest.NewRecorder()
			handler.ServeHTTP(rr, req)
			if rr.Code != tc.expectedStatus {
				t.Errorf("expected status %d, got %d", tc.expectedStatus, rr.Code)
			}
		})
	}
}

// TestNewMoLingServerGeneratesToken verifies that a random auth token is
// generated when ListenAddr is set and no token is provided in the config.
func TestNewMoLingServerGeneratesToken(t *testing.T) {
	mlConfig := config.MoLingConfig{
		BasePath:   filepath.Join(os.TempDir(), "moling_test"),
		ListenAddr: "127.0.0.1:0",
	}
	for _, dirName := range []string{"logs", "config", "browser", "data", "cache"} {
		_ = utils.CreateDirectory(filepath.Join(mlConfig.BasePath, dirName))
	}
	logger, ctx, err := comm.InitTestEnv()
	if err != nil {
		t.Fatalf("InitTestEnv: %v", err)
	}
	mlConfig.SetLogger(logger)

	srv, err := NewMoLingServer(ctx, []abstract.Service{}, mlConfig)
	if err != nil {
		t.Fatalf("NewMoLingServer: %v", err)
	}
	if srv.authToken == "" {
		t.Error("expected a non-empty auth token to be generated")
	}
}


================================================
FILE: pkg/services/abstract/abstract.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package abstract

import (
	"context"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"

	"github.com/gojue/moling/pkg/comm"
	"github.com/gojue/moling/pkg/config"
)

type ServiceFactory func(ctx context.Context) (Service, error)

// Service defines the interface for a service with various handlers and tools.
type Service interface {
	Ctx() context.Context
	// Resources returns a map of resources and their corresponding handler functions.
	Resources() map[mcp.Resource]server.ResourceHandlerFunc
	// ResourceTemplates returns a map of resource templates and their corresponding handler functions.
	ResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc
	// Prompts returns a map of prompts and their corresponding handler functions.
	Prompts() []PromptEntry
	// Tools returns a slice of server tools.
	Tools() []server.ServerTool
	// NotificationHandlers returns a map of notification handlers.
	NotificationHandlers() map[string]server.NotificationHandlerFunc

	// Config returns the configuration of the service as a string.
	Config() string
	// LoadConfig loads the configuration for the service from a map.
	LoadConfig(jsonData map[string]any) error

	// Init initializes the service with the given context and configuration.
	Init() error

	MlConfig() *config.MoLingConfig

	// Name returns the name of the service.
	Name() comm.MoLingServerType

	// Close closes the service and releases any resources it holds.
	Close() error
}


================================================
FILE: pkg/services/abstract/mlservice.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package abstract

import (
	"context"
	"sync"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/rs/zerolog"

	"github.com/gojue/moling/pkg/config"
	"github.com/gojue/moling/pkg/utils"
)

type PromptEntry struct {
	PromptVar   mcp.Prompt
	HandlerFunc server.PromptHandlerFunc
}

func (pe *PromptEntry) Prompt() mcp.Prompt {
	return pe.PromptVar
}

func (pe *PromptEntry) Handler() server.PromptHandlerFunc {
	return pe.HandlerFunc
}

// NewMLService creates a new MLService with the given context and logger.
func NewMLService(ctx context.Context, logger zerolog.Logger, cfg *config.MoLingConfig) MLService {
	return MLService{
		Context:  ctx,
		Logger:   logger,
		mlConfig: cfg,
	}
}

// MLService implements the Service interface and provides methods to manage resources, templates, prompts, tools, and notification handlers.
type MLService struct {
	Context context.Context
	Logger  zerolog.Logger // The logger for the service

	lock                 *sync.Mutex
	resources            map[mcp.Resource]server.ResourceHandlerFunc
	resourcesTemplates   map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc
	prompts              []PromptEntry
	tools                []server.ServerTool
	notificationHandlers map[string]server.NotificationHandlerFunc
	mlConfig             *config.MoLingConfig // The configuration for the service
}

// InitResources initializes the MLService with empty maps and a mutex.
func (mls *MLService) InitResources() error {
	mls.lock = &sync.Mutex{}
	mls.resources = make(map[mcp.Resource]server.ResourceHandlerFunc)
	mls.resourcesTemplates = make(map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc)
	mls.prompts = make([]PromptEntry, 0)
	mls.notificationHandlers = make(map[string]server.NotificationHandlerFunc)
	mls.tools = []server.ServerTool{}
	return nil
}

// Ctx returns the context of the MLService.
func (mls *MLService) Ctx() context.Context {
	return mls.Context
}

// AddResource adds a resource and its handler function to the service.
func (mls *MLService) AddResource(rs mcp.Resource, hr server.ResourceHandlerFunc) {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	mls.resources[rs] = hr
}

// AddResourceTemplate adds a resource template and its handler function to the service.
func (mls *MLService) AddResourceTemplate(rt mcp.ResourceTemplate, hr server.ResourceTemplateHandlerFunc) {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	mls.resourcesTemplates[rt] = hr
}

// AddPrompt adds a prompt and its handler function to the service.
func (mls *MLService) AddPrompt(pe PromptEntry) {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	mls.prompts = append(mls.prompts, pe)
}

// AddTool adds a tool and its handler function to the service.
func (mls *MLService) AddTool(tool mcp.Tool, handler server.ToolHandlerFunc) {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	mls.tools = append(mls.tools, server.ServerTool{Tool: tool, Handler: handler})
}

// AddNotificationHandler adds a notification handler to the service.
func (mls *MLService) AddNotificationHandler(name string, handler server.NotificationHandlerFunc) {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	mls.notificationHandlers[name] = handler
}

// Resources returns the map of resources and their handler functions.
func (mls *MLService) Resources() map[mcp.Resource]server.ResourceHandlerFunc {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	return mls.resources
}

// ResourceTemplates returns the map of resource templates and their handler functions.
func (mls *MLService) ResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	return mls.resourcesTemplates
}

// Prompts returns the map of prompts and their handler functions.
func (mls *MLService) Prompts() []PromptEntry {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	return mls.prompts
}

// Tools returns the slice of server tools.
func (mls *MLService) Tools() []server.ServerTool {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	return mls.tools
}

// NotificationHandlers returns the map of notification handlers.
func (mls *MLService) NotificationHandlers() map[string]server.NotificationHandlerFunc {
	mls.lock.Lock()
	defer mls.lock.Unlock()
	return mls.notificationHandlers
}

// MlConfig returns the configuration of the MoLing service.
func (mls *MLService) MlConfig() *config.MoLingConfig {
	return mls.mlConfig
}

// Config returns the configuration of the service as a string.
func (mls *MLService) Config() string {
	panic("not implemented yet") // TODO: Implement
}

// Name returns the name of the service.
func (mls *MLService) Name() string {
	panic("not implemented yet") // TODO: Implement
}

// LoadConfig loads the configuration for the service from a map.
func (mls *MLService) LoadConfig(jsonData map[string]any) error {
	//panic("not implemented yet") // TODO: Implement
	err := utils.MergeJSONToStruct(mls.mlConfig, jsonData)
	if err != nil {
		return err
	}
	return mls.mlConfig.Check()
}


================================================
FILE: pkg/services/abstract/mlservice_test.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package abstract

import (
	"context"
	"testing"

	"github.com/mark3labs/mcp-go/mcp"
)

func TestMLService_AddResource(t *testing.T) {
	service := &MLService{}
	err := service.InitResources()
	if err != nil {
		t.Fatalf("Failed to initialize MLService: %s", err.Error())
	}
	resource := mcp.Resource{Name: "testResource"}
	handler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				Text:     "text",
				URI:      "uri",
				MIMEType: "text/plain",
			},
		}, nil
	}

	service.AddResource(resource, handler)

	if len(service.resources) != 1 {
		t.Errorf("Expected 1 resource, got %d", len(service.resources))
	}
	if service.resources[resource] == nil {
		t.Errorf("Handler for resource not found")
	}
}

func TestMLService_AddResourceTemplate(t *testing.T) {
	service := &MLService{}
	err := service.InitResources()
	if err != nil {
		t.Fatalf("Failed to initialize MLService: %s", err.Error())
	}
	template := mcp.ResourceTemplate{Name: "testTemplate"}
	handler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				Text:     "text",
				URI:      "uri",
				MIMEType: "text/plain",
			},
		}, nil
	}

	service.AddResourceTemplate(template, handler)

	if len(service.resourcesTemplates) != 1 {
		t.Errorf("Expected 1 resource template, got %d", len(service.resourcesTemplates))
	}
	if service.resourcesTemplates[template] == nil {
		t.Errorf("Handler for resource template not found")
	}
}

func TestMLService_AddPrompt(t *testing.T) {
	service := &MLService{}
	err := service.InitResources()
	if err != nil {
		t.Fatalf("Failed to initialize MLService: %s", err.Error())
	}
	prompt := "testPrompt"
	handler := func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
		pms := make([]mcp.PromptMessage, 0)
		pms = append(pms, mcp.PromptMessage{
			Role: mcp.RoleUser,
			Content: mcp.TextContent{
				Type: "text",
				Text: "Prompt response",
			},
		})
		return &mcp.GetPromptResult{
			Description: "prompt description",
			Messages:    pms,
		}, nil
	}
	pe := PromptEntry{
		PromptVar:   mcp.Prompt{Name: "testPrompt"},
		HandlerFunc: handler,
	}
	service.AddPrompt(pe)

	if len(service.prompts) != 1 {
		t.Errorf("Expected 1 prompt, got %d", len(service.prompts))
	}
	for _, p := range service.prompts {
		if p.PromptVar.Name != prompt {
			t.Errorf("Expected prompt name %s, got %s", prompt, p.PromptVar.Name)
		}
	}
}

func TestMLService_AddTool(t *testing.T) {
	service := &MLService{}
	err := service.InitResources()
	if err != nil {
		t.Fatalf("Failed to initialize MLService: %s", err.Error())
	}
	tool := mcp.Tool{Name: "testTool"}
	handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		return &mcp.CallToolResult{
			Content: []mcp.Content{
				mcp.TextContent{
					Type: "text",
					Text: "Prompt response",
				},
			},
		}, nil
	}

	service.AddTool(tool, handler)

	if len(service.tools) != 1 {
		t.Errorf("Expected 1 tool, got %d", len(service.tools))
	}

	// After
	if service.tools[0].Tool.Name != tool.Name {
		t.Errorf("Tool not added correctly")
	}
}

func TestMLService_AddNotificationHandler(t *testing.T) {
	service := &MLService{}
	err := service.InitResources()
	if err != nil {
		t.Fatalf("Failed to initialize MLService: %s", err.Error())
	}
	name := "testHandler"
	handler := func(ctx context.Context, n mcp.JSONRPCNotification) {
		t.Logf("Received notification: %s", n.Method)
	}

	service.AddNotificationHandler(name, handler)

	if len(service.notificationHandlers) != 1 {
		t.Errorf("Expected 1 notification handler, got %d", len(service.notificationHandlers))
	}
	if service.notificationHandlers[name] == nil {
		t.Errorf("Handler for notification not found")
	}
}


================================================
FILE: pkg/services/browser/browser.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

// Package services provides a set of services for the MoLing application.
package browser

import (
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/chromedp/chromedp"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/rs/zerolog"

	"github.com/gojue/moling/pkg/comm"
	"github.com/gojue/moling/pkg/config"
	"github.com/gojue/moling/pkg/services/abstract"
	"github.com/gojue/moling/pkg/utils"
)

const (
	BrowserDataPath                         = "browser" // Path to store browser data
	BrowserServerName comm.MoLingServerType = "Browser"
)

// BrowserServer represents the configuration for the browser service.
type BrowserServer struct {
	abstract.MLService
	config       *BrowserConfig
	name         string // The name of the service
	cancelAlloc  context.CancelFunc
	cancelChrome context.CancelFunc
}

// NewBrowserServer creates a new BrowserServer instance with the given context and configuration.
func NewBrowserServer(ctx context.Context) (abstract.Service, error) {
	bc := NewBrowserConfig()
	globalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)
	bc.BrowserDataPath = filepath.Join(globalConf.BasePath, BrowserDataPath)
	bc.DataPath = filepath.Join(globalConf.BasePath, "data")
	logger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)
	if !ok {
		return nil, fmt.Errorf("BrowserServer: invalid logger type: %T", ctx.Value(comm.MoLingLoggerKey))
	}
	loggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
		e.Str("Service", string(BrowserServerName))
	})
	bs := &BrowserServer{
		MLService: abstract.NewMLService(ctx, logger.Hook(loggerNameHook), globalConf),
		config:    bc,
	}

	err := bs.InitResources()
	if err != nil {
		return nil, err
	}

	return bs, nil
}

// Init initializes the browser server by creating a new context.
func (bs *BrowserServer) Init() error {
	// Initialize the browser server
	err := bs.initBrowser(bs.config.BrowserDataPath)
	if err != nil {
		return fmt.Errorf("failed to initialize browser: %w", err)
	}
	err = utils.CreateDirectory(bs.config.DataPath)
	if err != nil {
		return fmt.Errorf("failed to create data directory: %w", err)
	}

	// Create a new context for the browser
	opts := append(
		chromedp.DefaultExecAllocatorOptions[:],
		chromedp.UserAgent(bs.config.UserAgent),
		chromedp.Flag("lang", bs.config.DefaultLanguage),
		chromedp.Flag("disable-blink-features", "AutomationControlled"),
		chromedp.Flag("enable-automation", false),
		chromedp.Flag("disable-features", "Translate"),
		chromedp.Flag("hide-scrollbars", false),
		chromedp.Flag("mute-audio", true),
		//chromedp.Flag("no-sandbox", true),
		chromedp.Flag("disable-infobars", true),
		chromedp.Flag("disable-extensions", true),
		chromedp.Flag("CommandLineFlagSecurityWarningsEnabled", false),
		chromedp.Flag("disable-notifications", true),
		chromedp.Flag("disable-dev-shm-usage", true),
		chromedp.Flag("autoplay-policy", "user-gesture-required"),
		chromedp.CombinedOutput(bs.Logger),
		// (1920, 1080), (1366, 768), (1440, 900), (1280, 800)
		chromedp.WindowSize(1280, 800),
		chromedp.UserDataDir(bs.config.BrowserDataPath),
		chromedp.IgnoreCertErrors,
	)

	// headless mode
	if bs.config.Headless {
		opts = append(opts, chromedp.Flag("headless", true))
		opts = append(opts, chromedp.Flag("disable-gpu", true))
		opts = append(opts, chromedp.Flag("disable-webgl", true))
	}

	bs.Context, bs.cancelAlloc = chromedp.NewExecAllocator(context.Background(), opts...)

	bs.Context, bs.cancelChrome = chromedp.NewContext(bs.Context,
		chromedp.WithErrorf(bs.Logger.Error().Msgf),
		chromedp.WithDebugf(bs.Logger.Debug().Msgf),
	)

	pe := abstract.PromptEntry{
		PromptVar: mcp.Prompt{
			Name:        "browser_prompt",
			Description: "Get the relevant functions and prompts of the Browser MCP Server",
			//Arguments:   make([]mcp.PromptArgument, 0),
		},
		HandlerFunc: bs.handlePrompt,
	}
	bs.AddPrompt(pe)
	bs.AddTool(mcp.NewTool(
		"browser_navigate",
		mcp.WithDescription("Navigate to a URL"),
		mcp.WithTitleAnnotation("Navigate Browser"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("url",
			mcp.Description("URL to navigate to"),
			mcp.Required(),
		),
	), bs.handleNavigate)
	bs.AddTool(mcp.NewTool(
		"browser_screenshot",
		mcp.WithDescription("Take a screenshot of the current page or a specific element"),
		mcp.WithTitleAnnotation("Take Screenshot"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("name",
			mcp.Description("Name for the screenshot"),
			mcp.Required(),
		),
		mcp.WithString("selector",
			mcp.Description("CSS selector for element to screenshot"),
		),
		mcp.WithNumber("width",
			mcp.Description("Width in pixels (default: 1700)"),
		),
		mcp.WithNumber("height",
			mcp.Description("Height in pixels (default: 1100)"),
		),
	), bs.handleScreenshot)
	bs.AddTool(mcp.NewTool(
		"browser_click",
		mcp.WithDescription("Click an element on the page"),
		mcp.WithTitleAnnotation("Click Element"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("selector",
			mcp.Description("CSS selector for element to click"),
			mcp.Required(),
		),
	), bs.handleClick)
	bs.AddTool(mcp.NewTool(
		"browser_fill",
		mcp.WithDescription("Fill out an input field"),
		mcp.WithTitleAnnotation("Fill Input"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("selector",
			mcp.Description("CSS selector for input field"),
			mcp.Required(),
		),
		mcp.WithString("value",
			mcp.Description("Value to fill"),
			mcp.Required(),
		),
	), bs.handleFill)
	bs.AddTool(mcp.NewTool(
		"browser_select",
		mcp.WithDescription("Select an element on the page with Select tag"),
		mcp.WithTitleAnnotation("Select Option"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("selector",
			mcp.Description("CSS selector for element to select"),
			mcp.Required(),
		),
		mcp.WithString("value",
			mcp.Description("Value to select"),
			mcp.Required(),
		),
	), bs.handleSelect)
	bs.AddTool(mcp.NewTool(
		"browser_hover",
		mcp.WithDescription("Hover an element on the page"),
		mcp.WithTitleAnnotation("Hover Element"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("selector",
			mcp.Description("CSS selector for element to hover"),
			mcp.Required(),
		),
	), bs.handleHover)
	bs.AddTool(mcp.NewTool(
		"browser_evaluate",
		mcp.WithDescription("Execute JavaScript in the browser console"),
		mcp.WithTitleAnnotation("Evaluate JavaScript"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("script",
			mcp.Description("JavaScript code to execute"),
			mcp.Required(),
		),
	), bs.handleEvaluate)

	bs.AddTool(mcp.NewTool(
		"browser_debug_enable",
		mcp.WithDescription("Enable JavaScript debugging"),
		mcp.WithTitleAnnotation("Toggle Debugging"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithBoolean("enabled",
			mcp.Description("Enable or disable debugging"),
			mcp.Required(),
		),
	), bs.handleDebugEnable)

	bs.AddTool(mcp.NewTool(
		"browser_set_breakpoint",
		mcp.WithDescription("Set a JavaScript breakpoint"),
		mcp.WithTitleAnnotation("Set Breakpoint"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("url",
			mcp.Description("URL of the script"),
			mcp.Required(),
		),
		mcp.WithNumber("line",
			mcp.Description("Line number"),
			mcp.Required(),
		),
		mcp.WithNumber("column",
			mcp.Description("Column number (optional)"),
		),
		mcp.WithString("condition",
			mcp.Description("Breakpoint condition (optional)"),
		),
	), bs.handleSetBreakpoint)

	bs.AddTool(mcp.NewTool(
		"browser_remove_breakpoint",
		mcp.WithDescription("Remove a JavaScript breakpoint"),
		mcp.WithTitleAnnotation("Remove Breakpoint"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("breakpointId",
			mcp.Description("Breakpoint ID to remove"),
			mcp.Required(),
		),
	), bs.handleRemoveBreakpoint)

	bs.AddTool(mcp.NewTool(
		"browser_pause",
		mcp.WithDescription("Pause JavaScript execution"),
		mcp.WithTitleAnnotation("Pause Execution"),
		mcp.WithDestructiveHintAnnotation(true),
	), bs.handlePause)

	bs.AddTool(mcp.NewTool(
		"browser_resume",
		mcp.WithDescription("Resume JavaScript execution"),
		mcp.WithTitleAnnotation("Resume Execution"),
		mcp.WithDestructiveHintAnnotation(true),
	), bs.handleResume)

	bs.AddTool(mcp.NewTool(
		"browser_get_callstack",
		mcp.WithDescription("Get current call stack when paused"),
		mcp.WithTitleAnnotation("Get Call Stack"),
		mcp.WithReadOnlyHintAnnotation(true),
	), bs.handleGetCallstack)
	return nil
}

// init initializes the browser server by creating the user data directory.
func (bs *BrowserServer) initBrowser(userDataDir string) error {
	_, err := os.Stat(userDataDir)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to stat user data directory: %w", err)
	}

	// Check if the directory exists, if it does, we can reuse it
	if err == nil {
		//  判断浏览器运行锁
		singletonLock := filepath.Join(userDataDir, "SingletonLock")
		_, err = os.Stat(singletonLock)
		if err == nil {
			bs.Logger.Debug().Msg("Browser is already running, removing SingletonLock")
			err = os.RemoveAll(singletonLock)
			if err != nil {
				bs.Logger.Error().Str("Lock", singletonLock).Msgf("Browser can't work due to failed removal of SingletonLock: %s", err.Error())
			}
		}
		return nil
	}
	// Create the directory
	err = os.MkdirAll(userDataDir, 0755)
	if err != nil {
		return fmt.Errorf("failed to create user data directory: %w", err)
	}
	return nil
}

func (bs *BrowserServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	// 处理浏览器提示
	return &mcp.GetPromptResult{
		Description: "",
		Messages: []mcp.PromptMessage{
			{
				Role: mcp.RoleUser,
				Content: mcp.TextContent{
					Type: "text",
					Text: bs.config.prompt,
				},
			},
		},
	}, nil
}

// handleNavigate handles the navigation action.
func (bs *BrowserServer) handleNavigate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	url, ok := args["url"].(string)
	if !ok {
		return nil, fmt.Errorf("url must be a string")
	}

	err := chromedp.Run(bs.Context, chromedp.Navigate(url))
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to navigate: %s", err.Error())), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Navigated to %s", url)), nil
}

// handleScreenshot handles the screenshot action.
func (bs *BrowserServer) handleScreenshot(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	name, ok := args["name"].(string)
	if !ok {
		return mcp.NewToolResultError("name must be a string"), nil
	}
	selector, _ := args["selector"].(string)
	width, _ := args["width"].(int)
	height, _ := args["height"].(int)
	if width == 0 {
		width = 1280
	}
	if height == 0 {
		height = 800
	}
	var buf []byte
	var err error
	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
	defer cancelFunc()
	if selector == "" {
		err = chromedp.Run(runCtx, chromedp.FullScreenshot(&buf, 90))
	} else {
		err = chromedp.Run(bs.Context, chromedp.Screenshot(selector, &buf, chromedp.NodeVisible))
	}
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to take screenshot: %s", err.Error())), nil
	}

	newName := filepath.Join(bs.config.DataPath, fmt.Sprintf("%s_%d.png", strings.TrimRight(name, ".png"), rand.Int()))
	err = os.WriteFile(newName, buf, 0644)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to save screenshot: %s", err.Error())), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Screenshot saved to:%s", newName)), nil
}

// handleClick handles the click action on a specified element.
func (bs *BrowserServer) handleClick(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	selector, ok := args["selector"].(string)
	if !ok {
		return mcp.NewToolResultError(fmt.Sprintf("selector must be a string:%v", selector)), nil
	}
	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
	defer cancelFunc()
	err := chromedp.Run(runCtx,
		chromedp.WaitReady("body", chromedp.ByQuery), // 等待页面就绪
		chromedp.WaitVisible(selector, chromedp.ByQuery),
		chromedp.Click(selector, chromedp.NodeVisible),
	)
	if err != nil {
		return mcp.NewToolResultError(fmt.Errorf("failed to click element: %s", err.Error()).Error()), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Clicked element %s", selector)), nil
}

// handleFill handles the fill action on a specified input field.
func (bs *BrowserServer) handleFill(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	selector, ok := args["selector"].(string)
	if !ok {
		return mcp.NewToolResultError(fmt.Sprintf("failed to fill selector:%v", args["selector"])), nil
	}

	value, ok := args["value"].(string)
	if !ok {
		return mcp.NewToolResultError(fmt.Sprintf("failed to fill input field: %v, selector:%v", args["value"], selector)), nil
	}

	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
	defer cancelFunc()
	err := chromedp.Run(runCtx, chromedp.SendKeys(selector, value, chromedp.NodeVisible))
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to fill input field: %s", err.Error())), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Filled input %s with value %s", selector, value)), nil
}

func (bs *BrowserServer) handleSelect(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	selector, ok := args["selector"].(string)
	if !ok {
		return mcp.NewToolResultError(fmt.Sprintf("failed to select selector:%v", args["selector"])), nil
	}
	value, ok := args["value"].(string)
	if !ok {
		return mcp.NewToolResultError(fmt.Sprintf("failed to select value:%v", args["value"])), nil
	}
	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
	defer cancelFunc()
	err := chromedp.Run(runCtx, chromedp.SetValue(selector, value, chromedp.NodeVisible))
	if err != nil {
		return mcp.NewToolResultError(fmt.Errorf("failed to select value: %s", err.Error()).Error()), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Selected value %s for element %s", value, selector)), nil
}

// handleHover handles the hover action on a specified element.
func (bs *BrowserServer) handleHover(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	selector, ok := args["selector"].(string)
	if !ok {
		return mcp.NewToolResultError(fmt.Sprintf("selector must be a string:%v", selector)), nil
	}
	var res bool
	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
	defer cancelFunc()
	// Use json.Marshal to safely embed the selector in JS, preventing code injection.
	selectorJSON, err := json.Marshal(selector)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("invalid selector: %s", err.Error())), nil
	}
	err = chromedp.Run(runCtx, chromedp.Evaluate(`document.querySelector(`+string(selectorJSON)+`).dispatchEvent(new Event('mouseover'))`, &res))
	if err != nil {
		return mcp.NewToolResultError(fmt.Errorf("failed to hover over element: %s", err.Error()).Error()), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Hovered over element %s, result:%t", selector, res)), nil
}

func (bs *BrowserServer) handleEvaluate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	script, ok := args["script"].(string)
	if !ok {
		return mcp.NewToolResultError("script must be a string"), nil
	}
	var result any
	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
	defer cancelFunc()
	err := chromedp.Run(runCtx, chromedp.Evaluate(script, &result))
	if err != nil {
		return mcp.NewToolResultError(fmt.Errorf("failed to execute script: %s", err.Error()).Error()), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Script executed successfully: %v", result)), nil
}

func (bs *BrowserServer) Close() error {
	bs.Logger.Debug().Msg("Closing browser server")
	bs.cancelAlloc()
	bs.cancelChrome()
	// Cancel the context to stop the browser
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	return chromedp.Cancel(ctx)
}

// Config returns the configuration of the service as a string.
func (bs *BrowserServer) Config() string {
	cfg, err := json.Marshal(bs.config)
	if err != nil {
		bs.Logger.Err(err).Msg("failed to marshal config")
		return "{}"
	}
	return string(cfg)
}

func (bs *BrowserServer) Name() comm.MoLingServerType {
	return BrowserServerName
}

// LoadConfig loads the configuration from a JSON object.
func (bs *BrowserServer) LoadConfig(jsonData map[string]any) error {
	err := utils.MergeJSONToStruct(bs.config, jsonData)
	if err != nil {
		return err
	}
	return bs.config.Check()
}


================================================
FILE: pkg/services/browser/browser_config.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

// Package services provides a set of services for the MoLing application.
package browser

import (
	"fmt"
	"os"
	"path/filepath"
)

const BrowserPromptDefault = `
You are an AI-powered browser automation assistant capable of performing a wide range of web interactions and debugging tasks. Your capabilities include:

1. **Navigation**: Navigate to any specified URL to load web pages.

2. **Screenshot Capture**: Take full-page screenshots or capture specific elements using CSS selectors, with customizable dimensions (default: 1700x1100 pixels).

3. **Element Interaction**:
   - Click on elements identified by CSS selectors
   - Hover over specified elements
   - Fill input fields with provided values
   - Select options in dropdown menus

4. **JavaScript Execution**:
   - Run arbitrary JavaScript code in the browser context
   - Evaluate scripts and return results

5. **Debugging Tools**:
   - Enable/disable JavaScript debugging mode
   - Set breakpoints at specific script locations (URL + line number + optional column/condition)
   - Remove existing breakpoints by ID
   - Pause and resume script execution
   - Retrieve current call stack when paused

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.

Please provide clear instructions including:
- The specific action you want performed
- Required parameters (URLs, selectors, values, etc.)
- Any optional parameters (dimensions, conditions, etc.)
- Expected outcomes where relevant

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.
`

type BrowserConfig struct {
	PromptFile           string `json:"prompt_file"` // PromptFile is the prompt file for the browser.
	prompt               string
	Headless             bool   `json:"headless"`
	Timeout              int    `json:"timeout"`
	Proxy                string `json:"proxy"`
	UserAgent            string `json:"user_agent"`
	DefaultLanguage      string `json:"default_language"`
	URLTimeout           int    `json:"url_timeout"`            // URLTimeout is the timeout for loading a URL. time.Second
	SelectorQueryTimeout int    `json:"selector_query_timeout"` // SelectorQueryTimeout is the timeout for CSS selector queries. time.Second
	DataPath             string `json:"data_path"`              // DataPath is the path to the data directory.
	BrowserDataPath      string `json:"browser_data_path"`      // BrowserDataPath is the path to the browser data directory.
}

func (cfg *BrowserConfig) Check() error {
	cfg.prompt = BrowserPromptDefault
	if cfg.Timeout <= 0 {
		return fmt.Errorf("timeout must be greater than 0")
	}
	if cfg.URLTimeout <= 0 {
		return fmt.Errorf("URL timeout must be greater than 0")
	}
	if cfg.SelectorQueryTimeout <= 0 {
		return fmt.Errorf("selector Query timeout must be greater than 0")
	}
	if cfg.PromptFile != "" {
		read, err := os.ReadFile(cfg.PromptFile)
		if err != nil {
			return fmt.Errorf("failed to read prompt file:%s, error: %w", cfg.PromptFile, err)
		}
		cfg.prompt = string(read)
	}
	return nil
}

// NewBrowserConfig creates a new BrowserConfig with default values.
func NewBrowserConfig() *BrowserConfig {
	return &BrowserConfig{
		Headless:             false,
		Timeout:              30,
		URLTimeout:           10,
		SelectorQueryTimeout: 10,
		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",
		DefaultLanguage:      "en-US",
		DataPath:             filepath.Join(os.TempDir(), ".moling", "data"),
	}
}


================================================
FILE: pkg/services/browser/browser_debugger.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

// Package services provides a set of services for the MoLing application.
package browser

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/chromedp/cdproto/target"
	"github.com/chromedp/chromedp"
	"github.com/mark3labs/mcp-go/mcp"
)

// handleDebugEnable handles the enabling and disabling of debugging in the browser.
func (bs *BrowserServer) handleDebugEnable(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	enabled, ok := args["enabled"].(bool)
	if !ok {
		return mcp.NewToolResultError("enabled must be a boolean"), nil
	}

	var err error
	rctx, cancel := context.WithCancel(bs.Context)
	defer cancel()

	if enabled {
		err = chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
			t := chromedp.FromContext(ctx).Target
			// 使用Execute方法执行AttachToTarget命令
			params := target.AttachToTarget(t.TargetID).WithFlatten(true)
			return t.Execute(ctx, "Target.attachToTarget", params, nil)
		}))
	} else {
		err = chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
			t := chromedp.FromContext(ctx).Target
			// 使用Execute方法执行DetachFromTarget命令
			params := target.DetachFromTarget().WithSessionID(t.SessionID)
			return t.Execute(ctx, "Target.detachFromTarget", params, nil)
		}))
	}

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to %s debugging: %v",
			map[bool]string{true: "enable", false: "disable"}[enabled], err)), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Debugging %s",
		map[bool]string{true: "enabled", false: "disabled"}[enabled])), nil
}

// handleSetBreakpoint handles setting a breakpoint in the browser.
func (bs *BrowserServer) handleSetBreakpoint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	url, ok := args["url"].(string)
	if !ok {
		return mcp.NewToolResultError("url must be a string"), nil
	}

	line, ok := args["line"].(float64)
	if !ok {
		return mcp.NewToolResultError("line must be a number"), nil
	}

	column, _ := args["column"].(float64)
	condition, _ := args["condition"].(string)

	var breakpointID string
	rctx, cancel := context.WithCancel(bs.Context)
	defer cancel()
	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
		t := chromedp.FromContext(ctx).Target
		params := map[string]any{
			"url":       url,
			"line":      int(line),
			"column":    int(column),
			"condition": condition,
		}

		var result map[string]any
		// 使用Execute方法执行Debugger.setBreakpoint命令
		if err := t.Execute(ctx, "Debugger.setBreakpoint", params, &result); err != nil {
			return err
		}

		breakpointID, ok = result["breakpointId"].(string)
		if !ok {
			breakpointID = ""
			return fmt.Errorf("failed to get breakpoint ID")
		}
		return nil
	}))

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to set breakpoint: %s", err.Error())), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Breakpoint set with ID: %s", breakpointID)), nil
}

// handleRemoveBreakpoint handles removing a breakpoint in the browser.
func (bs *BrowserServer) handleRemoveBreakpoint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	breakpointID, ok := args["breakpointId"].(string)
	if !ok {
		return mcp.NewToolResultError("breakpointId must be a string"), nil
	}
	rctx, cancel := context.WithCancel(bs.Context)
	defer cancel()
	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
		t := chromedp.FromContext(ctx).Target
		// 使用Execute方法执行Debugger.removeBreakpoint命令
		return t.Execute(ctx, "Debugger.removeBreakpoint", map[string]any{
			"breakpointId": breakpointID,
		}, nil)
	}))

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to remove breakpoint: %s", err.Error())), nil
	}
	return mcp.NewToolResultText(fmt.Sprintf("Breakpoint %s removed", breakpointID)), nil
}

// handlePause handles pausing the JavaScript execution in the browser.
func (bs *BrowserServer) handlePause(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	rctx, cancel := context.WithCancel(bs.Context)
	defer cancel()
	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
		t := chromedp.FromContext(ctx).Target
		// 使用Execute方法执行Debugger.pause命令
		return t.Execute(ctx, "Debugger.pause", nil, nil)
	}))

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to pause execution: %s", err.Error())), nil
	}
	return mcp.NewToolResultText("JavaScript execution paused"), nil
}

// handleResume handles resuming the JavaScript execution in the browser.
func (bs *BrowserServer) handleResume(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	rctx, cancel := context.WithCancel(bs.Context)
	defer cancel()
	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
		t := chromedp.FromContext(ctx).Target
		// 使用Execute方法执行Debugger.resume命令
		return t.Execute(ctx, "Debugger.resume", nil, nil)
	}))

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to resume execution: %s", err.Error())), nil
	}
	return mcp.NewToolResultText("JavaScript execution resumed"), nil
}

// handleStepOver handles stepping over the next line of JavaScript code in the browser.
func (bs *BrowserServer) handleGetCallstack(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	var callstack any
	rctx, cancel := context.WithCancel(bs.Context)
	defer cancel()
	err := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {
		t := chromedp.FromContext(ctx).Target
		// 使用Execute方法执行Debugger.getStackTrace命令
		return t.Execute(ctx, "Debugger.getStackTrace", nil, &callstack)
	}))

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to get call stack: %s", err.Error())), nil
	}

	callstackJSON, err := json.Marshal(callstack)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("failed to marshal call stack: %s", err.Error())), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Current call stack: %s", string(callstackJSON))), nil
}


================================================
FILE: pkg/services/browser/browser_test.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package browser

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/gojue/moling/pkg/comm"
)

func TestBrowserServer(t *testing.T) {
	//
	//cfg := &BrowserConfig{
	//	Headless:        true,
	//	Timeout:         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",
	//	DefaultLanguage: "en-US",
	//	URLTimeout:      10,
	//	SelectorQueryTimeout:      10,
	//}
	logger, ctx, err := comm.InitTestEnv()
	if err != nil {
		t.Fatalf("Failed to initialize test environment: %s", err.Error())
	}
	logger.Info().Msg("TestBrowserServer")

	_, err = NewBrowserServer(ctx)
	if err != nil {
		t.Fatalf("Failed to create BrowserServer: %s", err.Error())
	}
}

// TestHoverSelectorEscaping verifies that the selector is safely JSON-encoded
// before being embedded in the JavaScript expression, preventing code injection.
func TestHoverSelectorEscaping(t *testing.T) {
	tests := []struct {
		name     string
		selector string
		// wantPrefix checks that the selector is embedded as a JSON-encoded double-quoted string
		wantJSPrefix string
		wantJSSuffix string
	}{
		{
			name:         "normal selector",
			selector:     "body",
			wantJSPrefix: `document.querySelector("body").dispatchEvent`,
		},
		{
			name:     "injection attempt with single quotes and comma operator",
			selector: "body'),document.title='PWNED',document.querySelector('body",
			// After JSON encoding, the selector is a double-quoted string literal.
			// The single quotes and commas stay inside the string and are NOT executable.
			wantJSPrefix: `document.querySelector("body'),document.title='PWNED',document.querySelector('body").dispatchEvent`,
		},
		{
			name:         "injection attempt with semicolons and IIFE",
			selector:     "body'); (function(){ /* exfiltration */ })(); document.querySelector('body",
			wantJSPrefix: `document.querySelector("body'); (function(){ /* exfiltration */ })(); document.querySelector('body").dispatchEvent`,
		},
		{
			name:     "selector with double quotes is escaped by JSON",
			selector: `div[data-id="foo"]`,
			// json.Marshal escapes inner double quotes as \", so they cannot break out of the JS string.
			wantJSPrefix: `document.querySelector("div[data-id=\"foo\"]").dispatchEvent`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			selectorJSON, err := json.Marshal(tc.selector)
			if err != nil {
				t.Fatalf("json.Marshal failed: %v", err)
			}
			js := `document.querySelector(` + string(selectorJSON) + `).dispatchEvent(new Event('mouseover'))`

			// The embedded selector must be wrapped in JSON double-quotes (not single-quotes).
			// This ensures injected single-quote characters cannot break out of the JS string context.
			if !strings.HasPrefix(string(selectorJSON), `"`) || !strings.HasSuffix(string(selectorJSON), `"`) {
				t.Errorf("selector was not JSON-encoded as a double-quoted string: %s", string(selectorJSON))
			}

			if tc.wantJSPrefix != "" && !strings.HasPrefix(js, tc.wantJSPrefix) {
				t.Errorf("JS expression did not start with expected prefix\n  want: %s\n   got: %s", tc.wantJSPrefix, js)
			}
		})
	}
}


================================================
FILE: pkg/services/command/command.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

// Package services Description: This file contains the implementation of the CommandServer interface for macOS and  Linux.
package command

import (
	"context"
	"encoding/json"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/rs/zerolog"

	"github.com/gojue/moling/pkg/comm"
	"github.com/gojue/moling/pkg/config"
	"github.com/gojue/moling/pkg/services/abstract"
	"github.com/gojue/moling/pkg/utils"
)

var (
	// ErrCommandNotFound is returned when the command is not found.
	ErrCommandNotFound = fmt.Errorf("command not found")
	// ErrCommandNotAllowed is returned when the command is not allowed.
	ErrCommandNotAllowed = fmt.Errorf("command not allowed")
)

const (
	CommandServerName comm.MoLingServerType = "Command"
)

// CommandServer implements the Service interface and provides methods to execute named commands.
type CommandServer struct {
	abstract.MLService
	config    *CommandConfig
	osName    string
	osVersion string
}

// NewCommandServer creates a new CommandServer with the given allowed commands.
func NewCommandServer(ctx context.Context) (abstract.Service, error) {
	var err error
	cc := NewCommandConfig()
	gConf, ok := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)
	if !ok {
		return nil, fmt.Errorf("CommandServer: invalid config type")
	}

	lger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)
	if !ok {
		return nil, fmt.Errorf("CommandServer: invalid logger type")
	}

	loggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
		e.Str("Service", string(CommandServerName))
	})

	cs := &CommandServer{
		MLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), gConf),
		config:    cc,
	}

	err = cs.InitResources()
	if err != nil {
		return nil, err
	}

	return cs, nil
}

func (cs *CommandServer) Init() error {
	var err error
	pe := abstract.PromptEntry{
		PromptVar: mcp.Prompt{
			Name:        "command_prompt",
			Description: "get command prompt",
			//Arguments:   make([]mcp.PromptArgument, 0),
		},
		HandlerFunc: cs.handlePrompt,
	}
	cs.AddPrompt(pe)
	cs.AddTool(mcp.NewTool(
		"execute_command",
		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"),
		mcp.WithTitleAnnotation("Execute Command"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("command",
			mcp.Description("The command to execute"),
			mcp.Required(),
		),
	), cs.handleExecuteCommand)
	return err
}

func (cs *CommandServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	return &mcp.GetPromptResult{
		Description: "",
		Messages: []mcp.PromptMessage{
			{
				Role: mcp.RoleUser,
				Content: mcp.TextContent{
					Type: "text",
					Text: fmt.Sprintf(cs.config.prompt, cs.MlConfig().SystemInfo),
				},
			},
		},
	}, nil
}

// handleExecuteCommand handles the execution of a named command.
func (cs *CommandServer) handleExecuteCommand(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	command, ok := args["command"].(string)
	if !ok {
		return mcp.NewToolResultError(fmt.Errorf("command must be a string").Error()), nil
	}

	// Check if the command is allowed
	if !cs.isAllowedCommand(command) {
		cs.Logger.Err(ErrCommandNotAllowed).Str("command", command).Msgf("If you want to allow this command, add it to %s", filepath.Join(cs.MlConfig().BasePath, "config", cs.MlConfig().ConfigFile))
		return mcp.NewToolResultError(fmt.Sprintf("Error: Command '%s' is not allowed", command)), nil
	}

	// Execute the command
	output, err := ExecCommand(command)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Error executing command: %v", err)), nil
	}

	return mcp.NewToolResultText(output), nil
}

// shellInjectionPatterns are substrings that indicate potential command injection attempts.
// Commands containing any of these patterns are rejected before allowlist evaluation.
var shellInjectionPatterns = []string{";", "\n", "`", "$(", "${"}

// isAllowedCommand checks if the command is allowed based on the configuration.
func (cs *CommandServer) isAllowedCommand(command string) bool {
	command = strings.TrimSpace(command)
	if command == "" {
		return false
	}

	// Reject commands containing shell injection metacharacters.
	for _, pattern := range shellInjectionPatterns {
		if strings.Contains(command, pattern) {
			return false
		}
	}

	// Handle && (logical AND) — must be checked before single &.
	if strings.Contains(command, "&&") {
		parts := strings.Split(command, "&&")
		for _, part := range parts {
			if !cs.isAllowedCommand(strings.TrimSpace(part)) {
				return false
			}
		}
		return true
	}

	// Handle || (logical OR) — must be checked before single |.
	if strings.Contains(command, "||") {
		parts := strings.Split(command, "||")
		for _, part := range parts {
			if !cs.isAllowedCommand(strings.TrimSpace(part)) {
				return false
			}
		}
		return true
	}

	// Handle | (pipe).
	if strings.Contains(command, "|") {
		parts := strings.Split(command, "|")
		for _, part := range parts {
			if !cs.isAllowedCommand(strings.TrimSpace(part)) {
				return false
			}
		}
		return true
	}

	// Handle & (background execution).
	if strings.Contains(command, "&") {
		parts := strings.Split(command, "&")
		for _, part := range parts {
			if !cs.isAllowedCommand(strings.TrimSpace(part)) {
				return false
			}
		}
		return true
	}

	// Extract the command name (first token) and check it against the allowlist.
	fields := strings.Fields(command)
	if len(fields) == 0 {
		return false
	}
	cmdName := fields[0]
	for _, allowed := range cs.config.allowedCommands {
		if cmdName == strings.TrimSpace(allowed) {
			return true
		}
	}

	return false
}

// Config returns the configuration of the service as a string.
func (cs *CommandServer) Config() string {
	cs.config.AllowedCommand = strings.Join(cs.config.allowedCommands, ",")
	cfg, err := json.Marshal(cs.config)
	if err != nil {
		cs.Logger.Err(err).Msg("failed to marshal config")
		return "{}"
	}
	cs.Logger.Debug().Str("config", string(cfg)).Msg("CommandServer config")
	return string(cfg)
}

func (cs *CommandServer) Name() comm.MoLingServerType {
	return CommandServerName
}

func (cs *CommandServer) Close() error {
	// Cancel the context to stop the browser
	cs.Logger.Debug().Msg("CommandServer closed")
	return nil
}

// LoadConfig loads the configuration from a JSON object.
func (cs *CommandServer) LoadConfig(jsonData map[string]any) error {
	err := utils.MergeJSONToStruct(cs.config, jsonData)
	if err != nil {
		return err
	}
	// split the AllowedCommand string into a slice
	cs.config.allowedCommands = strings.Split(cs.config.AllowedCommand, ",")
	return cs.config.Check()
}


================================================
FILE: pkg/services/command/command_config.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package command

import (
	"fmt"
	"os"
	"strings"
)

const (
	CommandPromptDefault = `
You are a powerful terminal command assistant capable of executing various command-line on %s operations and management tasks. Your capabilities include:

1. **File and Directory Management**:
    - List files and subdirectories in a directory
    - Create new files or directories
    - Delete specified files or directories
    - Copy and move files and directories
    - Rename files or directories

2. **File Content Operations**:
    - View the contents of text files
    - Edit file contents
    - Redirect output to a file
    - Search file contents

3. **System Information Retrieval**:
    - Retrieve system information (e.g., CPU usage, memory usage, etc.)
    - View the current user and their permissions
    - Check the current working directory

4. **Network Operations**:
    - Check network connection status (e.g., using the ping command)
    - Query domain information (e.g., using the whois command)
    - Manage network services (e.g., start, stop, and restart services)

5. **Process Management**:
    - List currently running processes
    - Terminate specified processes
    - Adjust process priorities

Before executing any actions, please provide clear instructions, including:
- The specific command you want to execute
- Required parameters (file paths, directory names, etc.)
- Any optional parameters (e.g., modification options, output formats, etc.)
- Relevant expected results or output

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.
`
)

// CommandConfig represents the configuration for allowed commands.
type CommandConfig struct {
	PromptFile      string `json:"prompt_file"` // PromptFile is the prompt file for the command.
	prompt          string
	AllowedCommand  string `json:"allowed_command"` // AllowedCommand is a list of allowed command. split by comma. e.g. ls,cat,echo
	allowedCommands []string
}

var (
	allowedCmdDefault = []string{
		"ls", "cat", "echo", "pwd", "head", "tail", "grep", "find", "stat", "df",
		"du", "free", "top", "ps", "uptime", "who", "w", "last", "uname", "hostname",
		"ifconfig", "netstat", "ping", "traceroute", "route", "ip", "ss", "lsof", "vmstat",
		"iostat", "mpstat", "sar", "uptime", "cut", "sort", "uniq", "wc", "awk", "sed",
		"diff", "cmp", "comm", "file", "basename", "dirname", "chmod", "chown", "curl",
		"nslookup", "dig", "host", "ssh", "scp", "sftp", "ftp", "wget", "tar", "gzip",
		"scutil", "networksetup", "git", "cd",
	}
)

// NewCommandConfig creates a new CommandConfig with the given allowed commands.
func NewCommandConfig() *CommandConfig {
	return &CommandConfig{
		allowedCommands: allowedCmdDefault,
		AllowedCommand:  strings.Join(allowedCmdDefault, ","),
	}
}

// Check validates the allowed commands in the CommandConfig.
func (cc *CommandConfig) Check() error {
	cc.prompt = CommandPromptDefault
	var cnt int
	cnt = len(cc.allowedCommands)

	// Check if any command is empty
	for _, cmd := range cc.allowedCommands {
		if cmd == "" {
			cnt -= 1
		}
	}

	if cnt <= 0 {
		return fmt.Errorf("no allowed commands specified")
	}
	if cc.PromptFile != "" {
		read, err := os.ReadFile(cc.PromptFile)
		if err != nil {
			return fmt.Errorf("failed to read prompt file:%s, error: %w", cc.PromptFile, err)
		}
		cc.prompt = string(read)
	}
	return nil
}


================================================
FILE: pkg/services/command/command_exec.go
================================================
//go:build !windows

// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package command

import (
	"context"
	"errors"
	"os/exec"
	"time"
)

// ExecCommand executes a command and returns its output.
func ExecCommand(command string) (string, error) {
	var cmd *exec.Cmd
	ctx, cfunc := context.WithTimeout(context.Background(), time.Second*10)
	defer cfunc()
	cmd = exec.CommandContext(ctx, "sh", "-c", command)
	output, err := cmd.CombinedOutput()
	if err != nil {
		switch {
		case errors.Is(err, exec.ErrNotFound):
			// 命令未找到
			return "", errors.New("command not found")
		case errors.Is(ctx.Err(), context.DeadlineExceeded):
			// 超时时仅返回输出,不返回错误
			return string(output), nil
		default:
			return string(output), nil
		}
	}

	return string(output), nil
}


================================================
FILE: pkg/services/command/command_exec_test.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

package command

import (
	"context"
	"errors"
	"os/exec"
	"reflect"
	"testing"
	"time"

	"github.com/gojue/moling/pkg/comm"
)

// MockCommandServer is a mock implementation of CommandServer for testing purposes.
type MockCommandServer struct {
	CommandServer
}

// TestExecuteCommand tests the ExecCommand function.
func TestExecuteCommand(t *testing.T) {
	execCmd := "echo 'Hello, World!'"
	// Test a simple command
	output, err := ExecCommand(execCmd)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	expectedOutput := "Hello, World!\n"
	if output != expectedOutput {
		t.Errorf("Expected output %q, got %q", expectedOutput, output)
	}
	t.Logf("Command output: %s", output)
	// Test a command with a timeout
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*10)
	defer cancel()

	execCmd = "curl ifconfig.me|grep Time"
	output, err = ExecCommand(execCmd)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}
	t.Logf("Command output: %s", output)
	cmd := exec.CommandContext(ctx, "sleep", "1")
	err = cmd.Run()
	if err == nil {
		t.Fatalf("Expected timeout error, got nil")
	}
	if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
		t.Errorf("Expected context deadline exceeded error, got %v", ctx.Err())
	}
}

func TestAllowCmd(t *testing.T) {
	// Test with a command that is allowed
	_, ctx, err := comm.InitTestEnv()
	if err != nil {
		t.Fatalf("Failed to initialize test environment: %v", err)
	}

	cs, err := NewCommandServer(ctx)
	if err != nil {
		t.Fatalf("Failed to create CommandServer: %v", err)
	}

	cc := StructToMap(NewCommandConfig())
	t.Logf("CommandConfig: %v", cc)
	err = cs.LoadConfig(cc)
	if err != nil {
		t.Fatalf("Failed to load config: %v", err)
	}
	cmd := "cd /var/logs/notfound && git log --since=\"today\" --pretty=format:\"%h - %an, %ar : %s\""
	cs1 := cs.(*CommandServer)
	if !cs1.isAllowedCommand(cmd) {
		t.Errorf("Command '%s' is not allowed", cmd)
	}
	t.Log("Command is allowed:", cmd)
}

// TestIsAllowedCommandInjection verifies that shell injection attempts are blocked.
func TestIsAllowedCommandInjection(t *testing.T) {
	_, ctx, err := comm.InitTestEnv()
	if err != nil {
		t.Fatalf("Failed to initialize test environment: %v", err)
	}

	cs, err := NewCommandServer(ctx)
	if err != nil {
		t.Fatalf("Failed to create CommandServer: %v", err)
	}

	cc := StructToMap(NewCommandConfig())
	err = cs.LoadConfig(cc)
	if err != nil {
		t.Fatalf("Failed to load config: %v", err)
	}
	cs1 := cs.(*CommandServer)

	injectionAttempts := []struct {
		name    string
		command string
	}{
		{"semicolon injection", "echo hello; id"},
		{"semicolon with spaces", "echo hello ; whoami"},
		{"command substitution $()", "echo $(id)"},
		{"command substitution with cat", "echo $(cat /etc/passwd)"},
		{"backtick substitution", "echo `whoami`"},
		{"backtick substitution nested", "echo `id`"},
		{"newline injection", "echo hello\nid"},
		{"variable expansion ${}", "echo ${PATH}"},
		{"semicolon with allowed cmd", "ls; id"},
		{"semicolon chaining", "echo x; echo y; id"},
	}

	for _, tc := range injectionAttempts {
		t.Run(tc.name, func(t *testing.T) {
			if cs1.isAllowedCommand(tc.command) {
				t.Errorf("injection attempt should be blocked: %q", tc.command)
			}
		})
	}

	// Verify legitimate commands still work.
	legitimateCmds := []struct {
		name    string
		command string
	}{
		{"simple echo", "echo hello"},
		{"ls with flag", "ls -la"},
		{"pipe allowed cmds", "cat /etc/hostname | grep -v localhost"},
		{"logical AND allowed cmds", "echo hello && echo world"},
		{"logical OR allowed cmds", "echo hello || echo world"},
		{"git command", "git log --oneline"},
	}

	for _, tc := range legitimateCmds {
		t.Run(tc.name, func(t *testing.T) {
			if !cs1.isAllowedCommand(tc.command) {
				t.Errorf("legitimate command should be allowed: %q", tc.command)
			}
		})
	}
}

// 将 struct 转换为 map
func StructToMap(obj any) map[string]any {
	result := make(map[string]any)
	val := reflect.ValueOf(obj)
	if val.Kind() == reflect.Ptr {
		val = val.Elem()
	}
	if val.Kind() != reflect.Struct {
		return nil
	}
	typ := val.Type()
	for i := 0; i < val.NumField(); i++ {
		field := typ.Field(i)
		value := val.Field(i)
		// 跳过未导出的字段
		if field.PkgPath != "" {
			continue
		}
		// 获取字段的 json tag(如果存在)
		key := field.Name
		if tag := field.Tag.Get("json"); tag != "" {
			key = tag
		}
		result[key] = value.Interface()
	}
	return result
}


================================================
FILE: pkg/services/command/command_exec_windows.go
================================================
//go:build windows

// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling

// Package services Description: This file contains the implementation of the CommandServer interface for Windows.
package command

import (
	"os/exec"
)

// ExecCommand executes a command and returns its output.
func ExecCommand(command string) (string, error) {
	var cmd *exec.Cmd
	cmd = exec.Command("cmd", "/C", command)
	output, err := cmd.CombinedOutput()
	return string(output), err
}


================================================
FILE: pkg/services/filesystem/file_system.go
================================================
// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Repository: https://github.com/gojue/moling
// Source: https://github.com/mark3labs/mcp-filesystem-server

// Package services provides the implementation of the FileSystemServer, which allows access to files and directories on the local file system.
package filesystem

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/rs/zerolog"

	"github.com/gojue/moling/pkg/comm"
	"github.com/gojue/moling/pkg/config"
	"github.com/gojue/moling/pkg/services/abstract"
	"github.com/gojue/moling/pkg/utils"
)

const (
	// MaxInlineSize Maximum size for inline content (5MB)
	MaxInlineSize = 1024 * 1024 * 5
	// MaxBase64Size Maximum size for base64 encoding (1MB)
	MaxBase64Size = 1024 * 1024 * 1
)
const (
	FilesystemServerName comm.MoLingServerType = "FileSystem"
)

type FileInfo struct {
	Size        int64     `json:"size"`
	Created     time.Time `json:"created"`
	Modified    time.Time `json:"modified"`
	Accessed    time.Time `json:"accessed"`
	IsDirectory bool      `json:"isDirectory"`
	IsFile      bool      `json:"isFile"`
	Permissions string    `json:"permissions"`
}

type FilesystemServer struct {
	abstract.MLService
	config *FileSystemConfig
}

func NewFilesystemServer(ctx context.Context) (abstract.Service, error) {
	// Validate the config
	var err error
	globalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)
	userDataDir := filepath.Join(globalConf.BasePath, "data")

	fc := NewFileSystemConfig(userDataDir)

	lger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)
	if !ok {
		return nil, fmt.Errorf("FilesystemServer: invalid logger type")
	}

	loggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
		e.Str("Service", string(FilesystemServerName))
	})

	fs := &FilesystemServer{
		MLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), globalConf),
		config:    fc,
	}

	err = fs.InitResources()
	if err != nil {
		return nil, fmt.Errorf("failed to initialize filesystem server: %w", err)
	}

	return fs, nil
}

func (fs *FilesystemServer) Init() error {
	// Register resource handlers
	fs.AddResource(mcp.NewResource("file://", "File System",
		mcp.WithResourceDescription("Access to files and directories on the local file system"),
	), fs.handleReadResource)

	pe := abstract.PromptEntry{
		PromptVar: mcp.Prompt{
			Name:        "filesystem_prompt",
			Description: "Get the relevant functions and prompts of the FileSystem MCP Server.",
		},
		HandlerFunc: fs.handlePrompt,
	}
	fs.AddPrompt(pe)

	// Register tool handlers
	fs.AddTool(mcp.NewTool("read_file",
		mcp.WithDescription("Read the complete contents of a file from the file system."),
		mcp.WithTitleAnnotation("Read File"),
		mcp.WithReadOnlyHintAnnotation(true),
		mcp.WithString("path",
			mcp.Description("Relative path to the file to read"),
			mcp.Required(),
		),
	), fs.handleReadFile)

	fs.AddTool(mcp.NewTool(
		"write_file",
		mcp.WithDescription("Create a new file or overwrite an existing file with new content."),
		mcp.WithTitleAnnotation("Write File"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("path",
			mcp.Description("Relative Path where to write the file"),
			mcp.Required(),
		),
		mcp.WithString("content",
			mcp.Description("Content to write to the file"),
			mcp.Required(),
		),
	), fs.handleWriteFile)

	fs.AddTool(mcp.NewTool(
		"list_directory",
		mcp.WithDescription("Get a detailed listing of all files and directories in a specified path."),
		mcp.WithTitleAnnotation("List Directory"),
		mcp.WithReadOnlyHintAnnotation(true),
		mcp.WithString("path",
			mcp.Description("Relative Path of the directory to list"),
			mcp.Required(),
		),
	), fs.handleListDirectory)

	fs.AddTool(mcp.NewTool(
		"create_directory",
		mcp.WithDescription("Create a new directory or ensure a directory exists."),
		mcp.WithTitleAnnotation("Create Directory"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("path",
			mcp.Description("Relative Path of the directory to create"),
			mcp.Required(),
		),
	), fs.handleCreateDirectory)

	fs.AddTool(mcp.NewTool(
		"move_file",
		mcp.WithDescription("Move or rename files and directories."),
		mcp.WithTitleAnnotation("Move File"),
		mcp.WithDestructiveHintAnnotation(true),
		mcp.WithString("source",
			mcp.Description("Relative Source path of the file or directory"),
			mcp.Required(),
		),
		mcp.WithString("destination",
			mcp.Description("Relative Destination path"),
			mcp.Required(),
		),
	), fs.handleMoveFile)

	fs.AddTool(mcp.NewTool(
		"search_files",
		mcp.WithDescription("Recursively search for files and directories matching a pattern."),
		mcp.WithTitleAnnotation("Search Files"),
		mcp.WithReadOnlyHintAnnotation(true),
		mcp.WithString("path",
			mcp.Description("Relative Starting path for the search"),
			mcp.Required(),
		),
		mcp.WithString("pattern",
			mcp.Description("Relative Search pattern to match against file names"),
			mcp.Required(),
		),
	), fs.handleSearchFiles)

	fs.AddTool(mcp.NewTool(
		"get_file_info",
		mcp.WithDescription("Retrieve detailed metadata about a file or directory."),
		mcp.WithTitleAnnotation("Get File Info"),
		mcp.WithReadOnlyHintAnnotation(true),
		mcp.WithString("path",
			mcp.Description("Relative Path to the file or directory"),
			mcp.Required(),
		),
	), fs.handleGetFileInfo)

	fs.AddTool(mcp.NewTool(
		"list_allowed_directories",
		mcp.WithDescription("Returns the list of directories that this server is allowed to access."),
		mcp.WithTitleAnnotation("List Allowed Directories"),
		mcp.WithReadOnlyHintAnnotation(true),
	), fs.handleListAllowedDirectories)
	return nil
}

// handlePrompt handles the prompt request for the FilesystemServer
func (fs *FilesystemServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	return &mcp.GetPromptResult{
		Description: "",
		Messages: []mcp.PromptMessage{
			{
				Role: mcp.RoleUser,
				Content: mcp.TextContent{
					Type: "text",
					Text: fs.config.prompt,
				},
			},
		},
	}, nil
}

// isPathInAllowedDirs checks if a path is within any of the allowed directories
func (fs *FilesystemServer) isPathInAllowedDirs(path string) bool {
	// Ensure path is absolute and clean
	absPath, err := filepath.Abs(path)
	if err != nil {
		return false
	}

	// Add trailing separator to ensure we're checking a directory or a file within a directory
	// and not a prefix match (e.g., /tmp/foo should not match /tmp/foobar)
	if !strings.HasSuffix(absPath, string(filepath.Separator)) {
		// If it'fss a file, we need to check its directory
		if info, err := os.Stat(absPath); err == nil && !info.IsDir() {
			absPath = filepath.Dir(absPath) + string(filepath.Separator)
		} else {
			absPath = absPath + string(filepath.Separator)
		}
	}

	// Check if the path is within any of the allowed directories
	for _, dir := range fs.config.allowedDirs {
		if strings.HasPrefix(absPath, dir) {
			return true
		}
	}
	return false
}

func (fs *FilesystemServer) validatePath(requestedPath string) (string, error) {
	// Always convert to absolute path first
	var hasPrefix bool
	var firstDir string
	for _, dir := range fs.config.allowedDirs {
		if firstDir == "" {
			firstDir = dir
		}
		if strings.HasPrefix(requestedPath, dir) {
			hasPrefix = true
			break
		}
	}
	if !hasPrefix {
		requestedPath = filepath.Join(firstDir, requestedPath)
	}
	abs, err := filepath.Abs(requestedPath)
	if err != nil {
		return "", fmt.Errorf("invalid path: %w", err)
	}

	// Check if path is within allowed directories
	if !fs.isPathInAllowedDirs(abs) {
		return "", fmt.Errorf("access denied - path outside allowed directories: %s", abs)
	}

	// Handle symlinks
	realPath, err := filepath.EvalSymlinks(abs)
	if err != nil {
		if !os.IsNotExist(err) {
			return "", err
		}
		// For new files, check parent directory
		parent := filepath.Dir(abs)
		realParent, err := filepath.EvalSymlinks(parent)
		if err != nil {
			return "", fmt.Errorf("parent directory does not exist: %s", parent)
		}

		if !fs.isPathInAllowedDirs(realParent) {
			return "", fmt.Errorf(
				"access denied - parent directory outside allowed directories",
			)
		}
		return abs, nil
	}

	// Check if the real path (after resolving symlinks) is still within allowed directories
	if !fs.isPathInAllowedDirs(realPath) {
		return "", fmt.Errorf(
			"access denied - symlink target outside allowed directories",
		)
	}

	return realPath, nil
}

func (fs *FilesystemServer) getFileStats(path string) (FileInfo, error) {
	info, err := os.Stat(path)
	if err != nil {
		return FileInfo{}, err
	}

	return FileInfo{
		Size:        info.Size(),
		Created:     info.ModTime(), // Note: ModTime used as birth time isn't always available
		Modified:    info.ModTime(),
		Accessed:    info.ModTime(), // Note: Access time isn't always available
		IsDirectory: info.IsDir(),
		IsFile:      !info.IsDir(),
		Permissions: fmt.Sprintf("%o", info.Mode().Perm()),
	}, nil
}

func (fs *FilesystemServer) searchFiles(rootPath, pattern string) ([]string, error) {
	var results []string
	pattern = strings.ToLower(pattern)

	err := filepath.Walk(
		rootPath,
		func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return nil // Skip errors and continue
			}

			// Try to validate path
			if _, err := fs.validatePath(path); err != nil {
				return nil // Skip invalid paths
			}

			if strings.Contains(strings.ToLower(info.Name()), pattern) {
				results = append(results, path)
			}
			return nil
		},
	)
	if err != nil {
		return nil, err
	}
	return results, nil
}

// Resource handler
func (fs *FilesystemServer) handleReadResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
	uri := request.Params.URI
	fs.Logger.Debug().Str("uri", uri).Msg("handleReadResource")

	// Check if it'fss a file:// URI
	if !strings.HasPrefix(uri, "file://") {
		return nil, fmt.Errorf("unsupported URI scheme: %s", uri)
	}

	// Extract the path from the URI
	path := strings.TrimPrefix(uri, "file://")

	// Validate the path
	validPath, err := fs.validatePath(path)
	if err != nil {
		return nil, err
	}

	// Get file info
	fileInfo, err := os.Stat(validPath)
	if err != nil {
		return nil, err
	}

	// If it'fss a directory, return a listing
	if fileInfo.IsDir() {
		entries, err := os.ReadDir(validPath)
		if err != nil {
			return nil, err
		}

		var result strings.Builder
		result.WriteString(fmt.Sprintf("Directory listing for: %s\n\n", validPath))

		for _, entry := range entries {
			entryPath := filepath.Join(validPath, entry.Name())
			entryURI := utils.PathToResourceURI(entryPath)

			if entry.IsDir() {
				result.WriteString(fmt.Sprintf("[DIR]  %s (%s)\n", entry.Name(), entryURI))
			} else {
				info, err := entry.Info()
				if err == nil {
					result.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n",
						entry.Name(), entryURI, info.Size()))
				} else {
					result.WriteString(fmt.Sprintf("[FILE] %s (%s)\n", entry.Name(), entryURI))
				}
			}
		}

		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				URI:      uri,
				MIMEType: "text/plain",
				Text:     result.String(),
			},
		}, nil
	}

	// It'fss a file, determine how to handle it
	mimeType := utils.DetectMimeType(validPath)

	// Check file size
	if fileInfo.Size() > MaxInlineSize {
		// File is too large to inline, return a reference instead
		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				URI:      uri,
				MIMEType: "text/plain",
				Text:     fmt.Sprintf("File is too large to display inline (%d bytes). Use the read_file tool to access specific portions.", fileInfo.Size()),
			},
		}, nil
	}

	// Read the file content
	content, err := os.ReadFile(validPath)
	if err != nil {
		return nil, err
	}

	// Handle based on content type
	if utils.IsTextFile(mimeType) {
		// It'fss a text file, return as text
		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				URI:      uri,
				MIMEType: mimeType,
				Text:     string(content),
			},
		}, nil
	} else {
		// It'fss a binary file
		if fileInfo.Size() <= MaxBase64Size {
			// Small enough for base64 encoding
			return []mcp.ResourceContents{
				mcp.BlobResourceContents{
					URI:      uri,
					MIMEType: mimeType,
					Blob:     base64.StdEncoding.EncodeToString(content),
				},
			}, nil
		} else {
			// Too large for base64, return a reference
			return []mcp.ResourceContents{
				mcp.TextResourceContents{
					URI:      uri,
					MIMEType: "text/plain",
					Text:     fmt.Sprintf("Binary file (%s, %d bytes). Use the read_file tool to access specific portions.", mimeType, fileInfo.Size()),
				},
			}, nil
		}
	}
}

// Tool handlers

func (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	path, ok := args["path"].(string)
	if !ok {
		return mcp.NewToolResultError("Path must be a string"), nil
	}

	// 判断 前缀是不是已经包含了
	//path = filepath.Join(fss.config.CachePath, path)
	validPath, err := fs.validatePath(path)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("validate Path Error: %v", err)), nil
	}

	// Check if it'fss a directory
	info, err := os.Stat(validPath)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("check directory error: %v", err)), nil
	}

	if info.IsDir() {
		// For directories, return a resource reference instead
		resourceURI := utils.PathToResourceURI(validPath)
		return &mcp.CallToolResult{
			Content: []mcp.Content{
				mcp.TextContent{
					Type: "text",
					Text: fmt.Sprintf("This is a directory. Use the resource URI to browse its contents: %s", resourceURI),
				},
				mcp.EmbeddedResource{
					Type: "resource",
					Resource: mcp.TextResourceContents{
						URI:      resourceURI,
						MIMEType: "text/plain",
						Text:     fmt.Sprintf("Directory: %s", validPath),
					},
				},
			},
		}, nil
	}

	// Determine MIME type
	mimeType := utils.DetectMimeType(validPath)

	// Check file size
	if info.Size() > MaxInlineSize {
		// File is too large to inline, return a resource reference
		resourceURI := utils.PathToResourceURI(validPath)
		return &mcp.CallToolResult{
			Content: []mcp.Content{
				mcp.TextContent{
					Type: "text",
					Text: fmt.Sprintf("File is too large to display inline (%d bytes). Access it via resource URI: %s", info.Size(), resourceURI),
				},
				mcp.EmbeddedResource{
					Type: "resource",
					Resource: mcp.TextResourceContents{
						URI:      resourceURI,
						MIMEType: "text/plain",
						Text:     fmt.Sprintf("Large file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
					},
				},
			},
		}, nil
	}

	// Read file content
	content, err := os.ReadFile(validPath)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Error reading file: %v", err)), nil
	}

	// Handle based on content type
	if utils.IsTextFile(mimeType) {
		// It'fss a text file, return as text
		return mcp.NewToolResultText(string(content)), nil
	} else if utils.IsImageFile(mimeType) {
		// It'fss an image file, return as image content
		if info.Size() <= MaxBase64Size {
			return &mcp.CallToolResult{
				Content: []mcp.Content{
					mcp.TextContent{
						Type: "text",
						Text: fmt.Sprintf("Image file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
					},
					mcp.ImageContent{
						Type:     "image",
						Data:     base64.StdEncoding.EncodeToString(content),
						MIMEType: mimeType,
					},
				},
			}, nil
		} else {
			// Too large for base64, return a reference
			resourceURI := utils.PathToResourceURI(validPath)
			return &mcp.CallToolResult{
				Content: []mcp.Content{
					mcp.TextContent{
						Type: "text",
						Text: fmt.Sprintf("Image file is too large to display inline (%d bytes). Access it via resource URI: %s", info.Size(), resourceURI),
					},
					mcp.EmbeddedResource{
						Type: "resource",
						Resource: mcp.TextResourceContents{
							URI:      resourceURI,
							MIMEType: "text/plain",
							Text:     fmt.Sprintf("Large image: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
						},
					},
				},
			}, nil
		}
	} else {
		// It'fss another type of binary file
		resourceURI := utils.PathToResourceURI(validPath)

		if info.Size() <= MaxBase64Size {
			// Small enough for base64 encoding
			return &mcp.CallToolResult{
				Content: []mcp.Content{
					mcp.TextContent{
						Type: "text",
						Text: fmt.Sprintf("Binary file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
					},
					mcp.EmbeddedResource{
						Type: "resource",
						Resource: mcp.BlobResourceContents{
							URI:      resourceURI,
							MIMEType: mimeType,
							Blob:     base64.StdEncoding.EncodeToString(content),
						},
					},
				},
			}, nil
		} else {
			// Too large for base64, return a reference
			return &mcp.CallToolResult{
				Content: []mcp.Content{
					mcp.TextContent{
						Type: "text",
						Text: fmt.Sprintf("Binary file: %s (%s, %d bytes). Access it via resource URI: %s", validPath, mimeType, info.Size(), resourceURI),
					},
					mcp.EmbeddedResource{
						Type: "resource",
						Resource: mcp.TextResourceContents{
							URI:      resourceURI,
							MIMEType: "text/plain",
							Text:     fmt.Sprintf("Binary file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
						},
					},
				},
			}, nil
		}
	}
}

func (fs *FilesystemServer) handleWriteFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	path, ok := args["path"].(string)
	if !ok {
		return mcp.NewToolResultError("Path must be a string"), nil
	}
	content, ok := args["content"].(string)
	if !ok {
		return mcp.NewToolResultError("Content must be a string"), nil
	}

	//path = filepath.Join(fss.config.CachePath, path)

	validPath, err := fs.validatePath(path)
	if err != nil {
		return &mcp.CallToolResult{
			Content: []mcp.Content{
				mcp.TextContent{
					Type: "text",
					Text: fmt.Sprintf("Error: %v", err),
				},
			},
			IsError: true,
		}, nil
	}

	// Check if it'fss a directory
	if info, err := os.Stat(validPath); err == nil && info.IsDir() {
		return mcp.NewToolResultError(fmt.Sprintf("Error: Cannot write to a directory:%s", validPath)), nil
	}

	// Create parent directories if they don't exist
	parentDir := filepath.Dir(validPath)
	if err := os.MkdirAll(parentDir, 0755); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Error creating parent directories: %v", err)), nil
	}

	if err := os.WriteFile(validPath, []byte(content), 0644); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Error writing file: %v", err)), nil
	}

	// Get file info for the response
	info, err := os.Stat(validPath)
	if err != nil {
		// File was written but we couldn't get info
		return mcp.NewToolResultText(fmt.Sprintf("Successfully wrote to %s", path)), nil
	}

	resourceURI := utils.PathToResourceURI(validPath)
	return &mcp.CallToolResult{
		Content: []mcp.Content{
			mcp.TextContent{
				Type: "text",
				Text: fmt.Sprintf("Successfully wrote %d bytes to %s", info.Size(), path),
			},
			mcp.EmbeddedResource{
				Type: "resource",
				Resource: mcp.TextResourceContents{
					URI:      resourceURI,
					MIMEType: "text/plain",
					Text:     fmt.Sprintf("File: %s (%d bytes)", validPath, info.Size()),
				},
			},
		},
	}, nil
}

func (fs *FilesystemServer) handleListDirectory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	path, ok := args["path"].(string)
	if !ok {
		return mcp.NewToolResultError("Path must be a string"), nil
	}

	validPath, err := fs.validatePath(path)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("validate path error: %v, path:%s", err, validPath)), nil
	}

	// Check if it'fss a directory
	info, err := os.Stat(validPath)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Check directory %s Error: %v", validPath, err)), nil
	}

	if !info.IsDir() {
		return mcp.NewToolResultError(fmt.Sprintf("Error: Path is not a directory:%s", validPath)), nil
	}

	entries, err := os.ReadDir(validPath)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Error reading directory: %v", err)), nil
	}

	var result strings.Builder
	result.WriteString(fmt.Sprintf("Directory listing for: %s\n\n", validPath))

	for _, entry := range entries {
		entryPath := filepath.Join(validPath, entry.Name())
		resourceURI := utils.PathToResourceURI(entryPath)

		if entry.IsDir() {
			result.WriteString(fmt.Sprintf("[DIR]  %s (%s)\n", entry.Name(), resourceURI))
		} else {
			info, err := entry.Info()
			if err == nil {
				result.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n",
					entry.Name(), resourceURI, info.Size()))
			} else {
				result.WriteString(fmt.Sprintf("[FILE] %s (%s)\n", entry.Name(), resourceURI))
			}
		}
	}

	// Return both text content and embedded resource
	resourceURI := utils.PathToResourceURI(validPath)
	return &mcp.CallToolResult{
		Content: []mcp.Content{
			mcp.TextContent{
				Type: "text",
				Text: result.String(),
			},
			mcp.EmbeddedResource{
				Type: "resource",
				Resource: mcp.TextResourceContents{
					URI:      resourceURI,
					MIMEType: "text/plain",
					Text:     fmt.Sprintf("Directory: %s", validPath),
				},
			},
		},
	}, nil
}

func (fs *FilesystemServer) handleCreateDirectory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()
	path, ok := args["path"].(string)
	if !ok {
		return mcp.NewToolResultError("path must be a string"), nil
	}

	validPath, err := fs.validatePath(path)
	if err != nil {
		retur
Download .txt
gitextract_rez5qvem/

├── .github/
│   └── workflows/
│       ├── go-test.yml
│       └── release.yml
├── .gitignore
├── .golangci.yml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── Makefile.release
├── README.md
├── README_JA_JP.md
├── README_ZH_HANS.md
├── bin/
│   └── .gitkeep
├── cli/
│   ├── cmd/
│   │   ├── client.go
│   │   ├── config.go
│   │   ├── perrun.go
│   │   ├── root.go
│   │   └── utils.go
│   ├── cobrautl/
│   │   └── help.go
│   └── main.go
├── client/
│   ├── client.go
│   ├── client_config.go
│   ├── client_config_windows.go
│   └── client_test.go
├── dist/
│   └── .gitkeep
├── functions.mk
├── go.mod
├── go.sum
├── install/
│   ├── install.ps1
│   └── install.sh
├── main.go
├── pkg/
│   ├── comm/
│   │   ├── comm.go
│   │   └── errors.go
│   ├── config/
│   │   ├── config.go
│   │   ├── config_test.go
│   │   └── config_test.json
│   ├── server/
│   │   ├── server.go
│   │   └── server_test.go
│   ├── services/
│   │   ├── abstract/
│   │   │   ├── abstract.go
│   │   │   ├── mlservice.go
│   │   │   └── mlservice_test.go
│   │   ├── browser/
│   │   │   ├── browser.go
│   │   │   ├── browser_config.go
│   │   │   ├── browser_debugger.go
│   │   │   └── browser_test.go
│   │   ├── command/
│   │   │   ├── command.go
│   │   │   ├── command_config.go
│   │   │   ├── command_exec.go
│   │   │   ├── command_exec_test.go
│   │   │   └── command_exec_windows.go
│   │   ├── filesystem/
│   │   │   ├── file_system.go
│   │   │   ├── file_system_config.go
│   │   │   └── file_system_windows.go
│   │   └── register.go
│   └── utils/
│       ├── pid.go
│       ├── pid_unix.go
│       ├── pid_windows.go
│       ├── rotewriter.go
│       └── utils.go
├── prompts/
│   ├──  filesystem.md
│   ├── browser.md
│   └── command.md
└── variables.mk
Download .txt
SYMBOL INDEX (207 symbols across 37 files)

FILE: cli/cmd/client.go
  function ClientCommandFunc (line 50) | func ClientCommandFunc(command *cobra.Command, args []string) error {
  function init (line 76) | func init() {

FILE: cli/cmd/config.go
  function ConfigCommandFunc (line 48) | func ConfigCommandFunc(command *cobra.Command, args []string) error {
  function init (line 145) | func init() {

FILE: cli/cmd/perrun.go
  function mlsCommandPreFunc (line 28) | func mlsCommandPreFunc(cmd *cobra.Command, args []string) error {

FILE: cli/cmd/root.go
  constant CliName (line 45) | CliName            = "moling"
  constant CliNameZh (line 46) | CliNameZh          = "魔灵"
  constant MCPServerName (line 47) | MCPServerName      = "MoLing MCP Server"
  constant CliDescription (line 48) | CliDescription     = "MoLing is a computer-use and browser-use based MCP...
  constant CliDescriptionZh (line 49) | CliDescriptionZh   = "MoLing(魔灵)是一款基于computer-use和浏browser-use的 MCP 服务器,...
  constant CliHomepage (line 50) | CliHomepage        = "https://gojue.cc/moling"
  constant CliAuthor (line 51) | CliAuthor          = "CFC4N <cfc4ncs@gmail.com>"
  constant CliGithubRepo (line 52) | CliGithubRepo      = "https://github.com/gojue/moling"
  constant CliDescriptionLong (line 53) | CliDescriptionLong = `
  constant CliDescriptionLongZh (line 65) | CliDescriptionLongZh = `MoLing(魔灵)是一个computer-use的MCP Server,基于操作系统API实现...
  constant MLConfigName (line 78) | MLConfigName = "config.json"
  constant MLRootPath (line 79) | MLRootPath   = ".moling"
  constant MLPidName (line 80) | MLPidName    = "moling.pid"
  constant LogFileName (line 81) | LogFileName  = "moling.log"
  constant MaxLogSize (line 82) | MaxLogSize   = 1024 * 1024 * 512
  function usageFunc (line 116) | func usageFunc(c *cobra.Command) error {
  function Execute (line 122) | func Execute() {
  function init (line 135) | func init() {
  function initLogger (line 154) | func initLogger(mlDataPath string) zerolog.Logger {
  function mlsCommandFunc (line 173) | func mlsCommandFunc(command *cobra.Command, args []string) error {

FILE: cli/cobrautl/help.go
  function init (line 50) | func init() {
  function molingFlagUsages (line 99) | func molingFlagUsages(flagSet *pflag.FlagSet) string {
  function getSubCommands (line 132) | func getSubCommands(cmd *cobra.Command) []*cobra.Command {
  function UsageFunc (line 141) | func UsageFunc(cmd *cobra.Command, version string) error {
  function getTabOutWithWriter (line 164) | func getTabOutWithWriter(writer io.Writer) *tabwriter.Writer {

FILE: cli/main.go
  function Start (line 23) | func Start() {

FILE: client/client.go
  constant MCPServersKey (line 36) | MCPServersKey = "mcpServers"
  type MCPServerConfig (line 39) | type MCPServerConfig struct
  function NewMCPServerConfig (line 50) | func NewMCPServerConfig(description string, command string, srvName stri...
  type Manager (line 63) | type Manager struct
    method ListClient (line 81) | func (c *Manager) ListClient() {
    method SetupConfig (line 95) | func (c *Manager) SetupConfig() {
    method appendConfig (line 125) | func (c *Manager) appendConfig(name string, payload []byte) ([]byte, e...
    method checkExist (line 147) | func (c *Manager) checkExist(path string) bool {
  function NewManager (line 70) | func NewManager(lger zerolog.Logger, mcpConfig MCPServerConfig) (cm *Man...

FILE: client/client_config.go
  function init (line 30) | func init() {

FILE: client/client_config_windows.go
  function init (line 28) | func init() {

FILE: client/client_test.go
  function TestClientManager_ListClient (line 30) | func TestClientManager_ListClient(t *testing.T) {
  function TestClientManager_checkExist (line 96) | func TestClientManager_checkExist(t *testing.T) {

FILE: main.go
  function main (line 19) | func main() {

FILE: pkg/comm/comm.go
  type MoLingServerType (line 29) | type MoLingServerType
  type contextKey (line 31) | type contextKey
  constant MoLingConfigKey (line 35) | MoLingConfigKey contextKey = "moling_config"
  constant MoLingLoggerKey (line 36) | MoLingLoggerKey contextKey = "moling_logger"
  function InitTestEnv (line 40) | func InitTestEnv() (zerolog.Logger, context.Context, error) {

FILE: pkg/config/config.go
  type Config (line 24) | type Config interface
  type MoLingConfig (line 30) | type MoLingConfig struct
    method Check (line 52) | func (cfg *MoLingConfig) Check() error {
    method Logger (line 56) | func (cfg *MoLingConfig) Logger() zerolog.Logger {
    method SetLogger (line 60) | func (cfg *MoLingConfig) SetLogger(logger zerolog.Logger) {

FILE: pkg/config/config_test.go
  function TestConfigLoad (line 28) | func TestConfigLoad(t *testing.T) {

FILE: pkg/server/server.go
  type MoLingServer (line 38) | type MoLingServer struct
    method init (line 81) | func (m *MoLingServer) init() error {
    method loadService (line 93) | func (m *MoLingServer) loadService(srv abstract.Service) error {
    method Serve (line 206) | func (m *MoLingServer) Serve() error {
  function NewMoLingServer (line 48) | func NewMoLingServer(ctx context.Context, srvs []abstract.Service, mlCon...
  function requireJSONContentType (line 127) | func requireJSONContentType(next http.Handler) http.Handler {
  type corsRemoverResponseWriter (line 145) | type corsRemoverResponseWriter struct
    method cleanCORSHeader (line 159) | func (w *corsRemoverResponseWriter) cleanCORSHeader() {
    method WriteHeader (line 166) | func (w *corsRemoverResponseWriter) WriteHeader(code int) {
    method Write (line 171) | func (w *corsRemoverResponseWriter) Write(b []byte) (int, error) {
    method Flush (line 176) | func (w *corsRemoverResponseWriter) Flush() {
  function newCORSRemoverResponseWriter (line 151) | func newCORSRemoverResponseWriter(w http.ResponseWriter) *corsRemoverRes...
  function sseSecurityMiddleware (line 186) | func sseSecurityMiddleware(token string, next http.Handler) http.Handler {

FILE: pkg/server/server_test.go
  function TestNewMLServer (line 33) | func TestNewMLServer(t *testing.T) {
  function TestSSESecurityMiddleware (line 87) | func TestSSESecurityMiddleware(t *testing.T) {
  function TestRequireJSONContentType (line 156) | func TestRequireJSONContentType(t *testing.T) {
  function TestNewMoLingServerGeneratesToken (line 201) | func TestNewMoLingServerGeneratesToken(t *testing.T) {

FILE: pkg/services/abstract/abstract.go
  type ServiceFactory (line 29) | type ServiceFactory
  type Service (line 32) | type Service interface

FILE: pkg/services/abstract/mlservice.go
  type PromptEntry (line 31) | type PromptEntry struct
    method Prompt (line 36) | func (pe *PromptEntry) Prompt() mcp.Prompt {
    method Handler (line 40) | func (pe *PromptEntry) Handler() server.PromptHandlerFunc {
  function NewMLService (line 45) | func NewMLService(ctx context.Context, logger zerolog.Logger, cfg *confi...
  type MLService (line 54) | type MLService struct
    method InitResources (line 68) | func (mls *MLService) InitResources() error {
    method Ctx (line 79) | func (mls *MLService) Ctx() context.Context {
    method AddResource (line 84) | func (mls *MLService) AddResource(rs mcp.Resource, hr server.ResourceH...
    method AddResourceTemplate (line 91) | func (mls *MLService) AddResourceTemplate(rt mcp.ResourceTemplate, hr ...
    method AddPrompt (line 98) | func (mls *MLService) AddPrompt(pe PromptEntry) {
    method AddTool (line 105) | func (mls *MLService) AddTool(tool mcp.Tool, handler server.ToolHandle...
    method AddNotificationHandler (line 112) | func (mls *MLService) AddNotificationHandler(name string, handler serv...
    method Resources (line 119) | func (mls *MLService) Resources() map[mcp.Resource]server.ResourceHand...
    method ResourceTemplates (line 126) | func (mls *MLService) ResourceTemplates() map[mcp.ResourceTemplate]ser...
    method Prompts (line 133) | func (mls *MLService) Prompts() []PromptEntry {
    method Tools (line 140) | func (mls *MLService) Tools() []server.ServerTool {
    method NotificationHandlers (line 147) | func (mls *MLService) NotificationHandlers() map[string]server.Notific...
    method MlConfig (line 154) | func (mls *MLService) MlConfig() *config.MoLingConfig {
    method Config (line 159) | func (mls *MLService) Config() string {
    method Name (line 164) | func (mls *MLService) Name() string {
    method LoadConfig (line 169) | func (mls *MLService) LoadConfig(jsonData map[string]any) error {

FILE: pkg/services/abstract/mlservice_test.go
  function TestMLService_AddResource (line 26) | func TestMLService_AddResource(t *testing.T) {
  function TestMLService_AddResourceTemplate (line 53) | func TestMLService_AddResourceTemplate(t *testing.T) {
  function TestMLService_AddPrompt (line 80) | func TestMLService_AddPrompt(t *testing.T) {
  function TestMLService_AddTool (line 117) | func TestMLService_AddTool(t *testing.T) {
  function TestMLService_AddNotificationHandler (line 147) | func TestMLService_AddNotificationHandler(t *testing.T) {

FILE: pkg/services/browser/browser.go
  constant BrowserDataPath (line 41) | BrowserDataPath                         = "browser"
  constant BrowserServerName (line 42) | BrowserServerName comm.MoLingServerType = "Browser"
  type BrowserServer (line 46) | type BrowserServer struct
    method Init (line 81) | func (bs *BrowserServer) Init() error {
    method initBrowser (line 294) | func (bs *BrowserServer) initBrowser(userDataDir string) error {
    method handlePrompt (line 322) | func (bs *BrowserServer) handlePrompt(ctx context.Context, request mcp...
    method handleNavigate (line 339) | func (bs *BrowserServer) handleNavigate(ctx context.Context, request m...
    method handleScreenshot (line 354) | func (bs *BrowserServer) handleScreenshot(ctx context.Context, request...
    method handleClick (line 391) | func (bs *BrowserServer) handleClick(ctx context.Context, request mcp....
    method handleFill (line 411) | func (bs *BrowserServer) handleFill(ctx context.Context, request mcp.C...
    method handleSelect (line 432) | func (bs *BrowserServer) handleSelect(ctx context.Context, request mcp...
    method handleHover (line 452) | func (bs *BrowserServer) handleHover(ctx context.Context, request mcp....
    method handleEvaluate (line 473) | func (bs *BrowserServer) handleEvaluate(ctx context.Context, request m...
    method Close (line 489) | func (bs *BrowserServer) Close() error {
    method Config (line 500) | func (bs *BrowserServer) Config() string {
    method Name (line 509) | func (bs *BrowserServer) Name() comm.MoLingServerType {
    method LoadConfig (line 514) | func (bs *BrowserServer) LoadConfig(jsonData map[string]any) error {
  function NewBrowserServer (line 55) | func NewBrowserServer(ctx context.Context) (abstract.Service, error) {

FILE: pkg/services/browser/browser_config.go
  constant BrowserPromptDefault (line 26) | BrowserPromptDefault = `
  type BrowserConfig (line 61) | type BrowserConfig struct
    method Check (line 75) | func (cfg *BrowserConfig) Check() error {
  function NewBrowserConfig (line 97) | func NewBrowserConfig() *BrowserConfig {

FILE: pkg/services/browser/browser_debugger.go
  method handleDebugEnable (line 31) | func (bs *BrowserServer) handleDebugEnable(ctx context.Context, request ...
  method handleSetBreakpoint (line 67) | func (bs *BrowserServer) handleSetBreakpoint(ctx context.Context, reques...
  method handleRemoveBreakpoint (line 115) | func (bs *BrowserServer) handleRemoveBreakpoint(ctx context.Context, req...
  method handlePause (line 138) | func (bs *BrowserServer) handlePause(ctx context.Context, request mcp.Ca...
  method handleResume (line 154) | func (bs *BrowserServer) handleResume(ctx context.Context, request mcp.C...
  method handleGetCallstack (line 170) | func (bs *BrowserServer) handleGetCallstack(ctx context.Context, request...

FILE: pkg/services/browser/browser_test.go
  function TestBrowserServer (line 27) | func TestBrowserServer(t *testing.T) {
  function TestHoverSelectorEscaping (line 51) | func TestHoverSelectorEscaping(t *testing.T) {

FILE: pkg/services/command/command.go
  constant CommandServerName (line 44) | CommandServerName comm.MoLingServerType = "Command"
  type CommandServer (line 48) | type CommandServer struct
    method Init (line 86) | func (cs *CommandServer) Init() error {
    method handlePrompt (line 110) | func (cs *CommandServer) handlePrompt(ctx context.Context, request mcp...
    method handleExecuteCommand (line 126) | func (cs *CommandServer) handleExecuteCommand(ctx context.Context, req...
    method isAllowedCommand (line 153) | func (cs *CommandServer) isAllowedCommand(command string) bool {
    method Config (line 226) | func (cs *CommandServer) Config() string {
    method Name (line 237) | func (cs *CommandServer) Name() comm.MoLingServerType {
    method Close (line 241) | func (cs *CommandServer) Close() error {
    method LoadConfig (line 248) | func (cs *CommandServer) LoadConfig(jsonData map[string]any) error {
  function NewCommandServer (line 56) | func NewCommandServer(ctx context.Context) (abstract.Service, error) {

FILE: pkg/services/command/command_config.go
  constant CommandPromptDefault (line 26) | CommandPromptDefault = `
  type CommandConfig (line 68) | type CommandConfig struct
    method Check (line 96) | func (cc *CommandConfig) Check() error {
  function NewCommandConfig (line 88) | func NewCommandConfig() *CommandConfig {

FILE: pkg/services/command/command_exec.go
  function ExecCommand (line 29) | func ExecCommand(command string) (string, error) {

FILE: pkg/services/command/command_exec_test.go
  type MockCommandServer (line 31) | type MockCommandServer struct
  function TestExecuteCommand (line 36) | func TestExecuteCommand(t *testing.T) {
  function TestAllowCmd (line 68) | func TestAllowCmd(t *testing.T) {
  function TestIsAllowedCommandInjection (line 95) | func TestIsAllowedCommandInjection(t *testing.T) {
  function StructToMap (line 160) | func StructToMap(obj any) map[string]any {

FILE: pkg/services/command/command_exec_windows.go
  function ExecCommand (line 27) | func ExecCommand(command string) (string, error) {

FILE: pkg/services/filesystem/file_system.go
  constant MaxInlineSize (line 42) | MaxInlineSize = 1024 * 1024 * 5
  constant MaxBase64Size (line 44) | MaxBase64Size = 1024 * 1024 * 1
  constant FilesystemServerName (line 47) | FilesystemServerName comm.MoLingServerType = "FileSystem"
  type FileInfo (line 50) | type FileInfo struct
  type FilesystemServer (line 60) | type FilesystemServer struct
    method Init (line 95) | func (fs *FilesystemServer) Init() error {
    method handlePrompt (line 209) | func (fs *FilesystemServer) handlePrompt(ctx context.Context, request ...
    method isPathInAllowedDirs (line 225) | func (fs *FilesystemServer) isPathInAllowedDirs(path string) bool {
    method validatePath (line 252) | func (fs *FilesystemServer) validatePath(requestedPath string) (string...
    method getFileStats (line 309) | func (fs *FilesystemServer) getFileStats(path string) (FileInfo, error) {
    method searchFiles (line 326) | func (fs *FilesystemServer) searchFiles(rootPath, pattern string) ([]s...
    method handleReadResource (line 355) | func (fs *FilesystemServer) handleReadResource(ctx context.Context, re...
    method handleReadFile (line 472) | func (fs *FilesystemServer) handleReadFile(ctx context.Context, reques...
    method handleWriteFile (line 628) | func (fs *FilesystemServer) handleWriteFile(ctx context.Context, reque...
    method handleListDirectory (line 695) | func (fs *FilesystemServer) handleListDirectory(ctx context.Context, r...
    method handleCreateDirectory (line 762) | func (fs *FilesystemServer) handleCreateDirectory(ctx context.Context,...
    method handleMoveFile (line 821) | func (fs *FilesystemServer) handleMoveFile(ctx context.Context, reques...
    method handleSearchFiles (line 880) | func (fs *FilesystemServer) handleSearchFiles(ctx context.Context, req...
    method handleGetFileInfo (line 937) | func (fs *FilesystemServer) handleGetFileInfo(ctx context.Context, req...
    method handleListAllowedDirectories (line 1012) | func (fs *FilesystemServer) handleListAllowedDirectories(ctx context.C...
    method Config (line 1031) | func (fs *FilesystemServer) Config() string {
    method Name (line 1041) | func (fs *FilesystemServer) Name() comm.MoLingServerType {
    method Close (line 1045) | func (fs *FilesystemServer) Close() error {
    method LoadConfig (line 1052) | func (fs *FilesystemServer) LoadConfig(jsonData map[string]any) error {
  function NewFilesystemServer (line 65) | func NewFilesystemServer(ctx context.Context) (abstract.Service, error) {

FILE: pkg/services/filesystem/file_system_config.go
  constant FileSystemPromptDefault (line 28) | FileSystemPromptDefault = `
  type FileSystemConfig (line 67) | type FileSystemConfig struct
    method Check (line 92) | func (fc *FileSystemConfig) Check() error {
  function NewFileSystemConfig (line 76) | func NewFileSystemConfig(path string) *FileSystemConfig {

FILE: pkg/services/filesystem/file_system_windows.go
  function init (line 23) | func init() {

FILE: pkg/services/register.go
  function RegisterServ (line 30) | func RegisterServ(n comm.MoLingServerType, f abstract.ServiceFactory) {
  function ServiceList (line 35) | func ServiceList() map[comm.MoLingServerType]abstract.ServiceFactory {
  function init (line 39) | func init() {

FILE: pkg/utils/pid.go
  function CreatePIDFile (line 29) | func CreatePIDFile(pidFilePath string) error {
  function RemovePIDFile (line 67) | func RemovePIDFile(pidFilePath string) error {

FILE: pkg/utils/pid_unix.go
  function lockFile (line 29) | func lockFile(file *os.File) (bool, error) {
  function unlockFile (line 40) | func unlockFile(file *os.File) error {

FILE: pkg/utils/pid_windows.go
  constant LockfileExclusiveLock (line 37) | LockfileExclusiveLock   = 2
  constant LockfileFailImmediately (line 38) | LockfileFailImmediately = 1
  constant ErrorLockViolation (line 40) | ErrorLockViolation = syscall.Errno(33)
  function lockFile (line 43) | func lockFile(file *os.File) (bool, error) {
  function unlockFile (line 68) | func unlockFile(file *os.File) error {

FILE: pkg/utils/rotewriter.go
  type RotateWriter (line 27) | type RotateWriter struct
    method Write (line 65) | func (rw *RotateWriter) Write(p []byte) (n int, err error) {
    method CheckFileSize (line 75) | func (rw *RotateWriter) CheckFileSize() error {
    method Close (line 108) | func (rw *RotateWriter) Close() error {
  function NewRotateWriter (line 37) | func NewRotateWriter(filePath string, maxSize int64) (*RotateWriter, err...

FILE: pkg/utils/utils.go
  function CreateDirectory (line 30) | func CreateDirectory(path string) error {
  function StringInSlice (line 46) | func StringInSlice(s string, modules []string) bool {
  function MergeJSONToStruct (line 56) | func MergeJSONToStruct(target any, jsonMap map[string]any) error {
  function DetectMimeType (line 87) | func DetectMimeType(path string) string {
  function IsTextFile (line 116) | func IsTextFile(mimeType string) bool {
  function IsImageFile (line 127) | func IsImageFile(mimeType string) bool {
  function PathToResourceURI (line 132) | func PathToResourceURI(path string) string {
Condensed preview — 62 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (255K chars).
[
  {
    "path": ".github/workflows/go-test.yml",
    "chars": 1118,
    "preview": "name: Golang CI and Test\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build-o"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1652,
    "preview": "name: MoLing Release\non:\n  push:\n    tags:\n      - \"v*\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n"
  },
  {
    "path": ".gitignore",
    "chars": 25,
    "preview": "/services/mo*\n/bin/moling"
  },
  {
    "path": ".golangci.yml",
    "chars": 1649,
    "preview": "# This configuration file is not a recommendation.\n#\n# We intentionally use a limited set of linters.\n# This configurati"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 5978,
    "preview": "# v0.4.0 (2025-05-31)\n\n## What's Changed\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.3.2...v0.4.0\n<h"
  },
  {
    "path": "LICENSE",
    "chars": 11350,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "Makefile",
    "chars": 1648,
    "preview": "include variables.mk\ninclude functions.mk\n\n.PHONY: all | env\nall: clean build\n\t@echo $(shell date)\n\n.ONESHELL:\nSHELL = /"
  },
  {
    "path": "Makefile.release",
    "chars": 849,
    "preview": "include variables.mk\ninclude functions.mk\n\n.PHONY: all build package clean\n\n# 定义目标系统和架构\nTARGET_SYSTEMS := darwin linux w"
  },
  {
    "path": "README.md",
    "chars": 4626,
    "preview": "## MoLing MCP Server\n\nEnglish | [中文](./README_ZH_HANS.md) | [日本語](./README_JA_JP.md)\n\n[![GitHub stars](https://img.shiel"
  },
  {
    "path": "README_JA_JP.md",
    "chars": 3358,
    "preview": "## MoLing MCP サーバー\n\n[English](./README.md) | [中文](./README_ZH_HANS.md) | 日本語\n\n[![GitHub stars](https://img.shields.io/gi"
  },
  {
    "path": "README_ZH_HANS.md",
    "chars": 2954,
    "preview": "## MoLing MCP 服务器\n\n[English](./README.md) | 中文 | [日本語](./README_JA_JP.md)\n\n[![GitHub stars](https://img.shields.io/githu"
  },
  {
    "path": "bin/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cli/cmd/client.go",
    "chars": 2973,
    "preview": "/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Vers"
  },
  {
    "path": "cli/cmd/config.go",
    "chars": 4491,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "cli/cmd/perrun.go",
    "chars": 1163,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "cli/cmd/root.go",
    "chars": 10509,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "cli/cmd/utils.go",
    "chars": 12,
    "preview": "package cmd\n"
  },
  {
    "path": "cli/cobrautl/help.go",
    "chars": 3960,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "cli/main.go",
    "chars": 766,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "client/client.go",
    "chars": 4887,
    "preview": "/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Vers"
  },
  {
    "path": "client/client_config.go",
    "chars": 2409,
    "preview": "//go:build !windows\n\n/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the"
  },
  {
    "path": "client/client_config_windows.go",
    "chars": 2108,
    "preview": "/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Vers"
  },
  {
    "path": "client/client_test.go",
    "chars": 3249,
    "preview": "/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Vers"
  },
  {
    "path": "dist/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "functions.mk",
    "chars": 521,
    "preview": "define allow-override\n  $(if $(or $(findstring environment,$(origin $(1))),\\\n            $(findstring command line,$(ori"
  },
  {
    "path": "go.mod",
    "chars": 896,
    "preview": "module github.com/gojue/moling\n\ngo 1.24.1\n\nrequire (\n\tgithub.com/chromedp/cdproto v0.0.0-20250518235601-40b4c35ec9fe\n\tgi"
  },
  {
    "path": "go.sum",
    "chars": 6344,
    "preview": "github.com/chromedp/cdproto v0.0.0-20250518235601-40b4c35ec9fe h1:roGYW+2lkWq2EdEOrSOxj8+L07gG1q6iF3xeKUHfcDQ=\ngithub.co"
  },
  {
    "path": "install/install.ps1",
    "chars": 1614,
    "preview": "#!/usr/bin/env pwsh\n\nSet-StrictMode -Version Latest\n\nWrite-Output \"Welcome to MoLing MCP Server initialization script.\"\n"
  },
  {
    "path": "install/install.sh",
    "chars": 1534,
    "preview": "#!/bin/bash\n\nset -e\n\necho \"Welcome to MoLing MCP Server initialization script.\"\necho \"Home page: https://gojue.cc/moling"
  },
  {
    "path": "main.go",
    "chars": 705,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/comm/comm.go",
    "chars": 1799,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/comm/errors.go",
    "chars": 799,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/config/config.go",
    "chars": 2825,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/config/config_test.go",
    "chars": 1875,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/config/config_test.json",
    "chars": 526,
    "preview": "{\n  \"BrowserServer\": {\n  },\n  \"CommandServer\": {\n    \"allowed_commands\": [\n      \"ls\",\n      \"cat\",\n      \"echo\"\n    ]\n "
  },
  {
    "path": "pkg/server/server.go",
    "chars": 7739,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/server/server_test.go",
    "chars": 7651,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/abstract/abstract.go",
    "chars": 2159,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/abstract/mlservice.go",
    "chars": 5648,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/abstract/mlservice_test.go",
    "chars": 4572,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/browser/browser.go",
    "chars": 17828,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/browser/browser_config.go",
    "chars": 4543,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/browser/browser_debugger.go",
    "chars": 6849,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/browser/browser_test.go",
    "chars": 3859,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/command/command.go",
    "chars": 7502,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/command/command_config.go",
    "chars": 4163,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/command/command_exec.go",
    "chars": 1382,
    "preview": "//go:build !windows\n\n// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache "
  },
  {
    "path": "pkg/services/command/command_exec_test.go",
    "chars": 5092,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/command/command_exec_windows.go",
    "chars": 1086,
    "preview": "//go:build windows\n\n// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache L"
  },
  {
    "path": "pkg/services/filesystem/file_system.go",
    "chars": 30575,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/filesystem/file_system_config.go",
    "chars": 4057,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/services/filesystem/file_system_windows.go",
    "chars": 879,
    "preview": "/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2"
  },
  {
    "path": "pkg/services/register.go",
    "chars": 1601,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "pkg/utils/pid.go",
    "chars": 2184,
    "preview": "/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2"
  },
  {
    "path": "pkg/utils/pid_unix.go",
    "chars": 1157,
    "preview": "//go:build darwin || linux || freebsd || openbsd || netbsd\n\n/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights "
  },
  {
    "path": "pkg/utils/pid_windows.go",
    "chars": 1845,
    "preview": "//go:build windows\n\n/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apach"
  },
  {
    "path": "pkg/utils/rotewriter.go",
    "chars": 2541,
    "preview": "/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2"
  },
  {
    "path": "pkg/utils/utils.go",
    "chars": 3464,
    "preview": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 "
  },
  {
    "path": "prompts/ filesystem.md",
    "chars": 1923,
    "preview": "## 汉字\n\n你是一个功能强大的本地文件系统管理助手,能够执行多种文件操作和管理任务。你的能力包括:\n\n1. **文件浏览**:导航到指定目录以加载文件和文件夹列表。\n\n2. **文件操作**:\n    - 创建新文件或文件夹\n    - "
  },
  {
    "path": "prompts/browser.md",
    "chars": 2407,
    "preview": "## 汉字\n\n你是一个由人工智能驱动的浏览器自动化助手,能够执行广泛的网页交互和调试任务。你的能力包括:\n\n1. **导航**:导航到任何指定的URL以加载网页。\n\n2. **屏幕截图**:拍摄完整网页截图或使用CSS选择器捕获特定元素,支"
  },
  {
    "path": "prompts/command.md",
    "chars": 2251,
    "preview": "## 汉字\n\n你是{%system_os%}上的一个功能强大的终端命令助手,能够执行各种命令行操作和管理任务。你的能力包括:\n\n1. **文件与目录管理**:\n    - 列出目录中的文件和子目录\n    - 创建新文件或目录\n    - "
  },
  {
    "path": "variables.mk",
    "chars": 1533,
    "preview": "CMD_MAKE = make\nCMD_TR ?= tr\nCMD_CUT ?= cut\nCMD_AWK ?= awk\nCMD_SED ?= sed\nCMD_FILE ?= file\nCMD_GIT ?= git\nCMD_CLANG ?= c"
  }
]

About this extraction

This page contains the full source code of the gojue/moling GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 62 files (222.3 KB), approximately 64.0k tokens, and a symbol index with 207 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!