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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/images/logo.svg:
--------------------------------------------------------------------------------

```
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <svg id="svg" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 600">
 3 |     <!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186)  -->
 4 |     <defs>
 5 |         <style>
 6 |             .st0 {
 7 |             fill-rule: evenodd;
 8 |             }
 9 |         </style>
10 |     </defs>
11 |     <g id="shape_PUEPPPvLNT">
12 |         <path class="st0"
13 |               d="M431.5.8c-10.8,10.1-9.1,23.3,4.4,34.7,3.1,2.6,5.2,4.8,4.6,4.9-.6.1-4.2-.7-7.9-1.8-9.5-2.8-23.8-5.6-34-6.6l-8.5-.9-2.6-4.2c-3.1-4.9-8.2-9.1-13.1-10.8-3.2-1.1-3.7-1.7-4.9-5-1.5-4.2-4.6-7-9.4-8.4-2.6-.8-4-.7-8.1.3-9.3,2.4-14.1,6.4-16.5,14-1.2,3.8-1.5,4-7.4,6.8-13.7,6.6-18.5,14.5-13.8,22.7l1.8,3.2-1.6,1.6c-1.5,1.5-1.9,1.6-5,.7-1.9-.5-8.1-1-13.8-1h-10.4c0,0-.2,2-.2,2l-.2,2.1h26.2l-7.1,7.1c-8.7,8.8-13.4,15.6-17.9,26.1-6.8,15.7-8.5,22.4-11.1,45.3-1,8.4-2.1,14.8-3.2,17.6-1.6,4.1-1.6,4.5-.4,6.9,2.8,5.8,5.7,15.3,4.7,14.9-3.6-1.5-3.8-1.4-1.9.6,1.6,1.7,1.7,2.1.6,2.1s-2.2-.6-3.3-1.3c-2-1.3-2-1.3-1.4.6,1.1,3.7,3.5,5.4,7.4,5.4s3.7.3,4,1.6c.3,1.1-.3,2.1-2.3,3.6-3,2.3-2.4,3.3,1.8,3.3,1.4,0,2.5.3,2.5.6,0,.3-.9,4.2-1.9,8.7-1,4.5-1.9,10.5-1.9,13.4,0,14.1,8.9,39.5,20.8,59.2,3,4.9,6.5,11.8,7.8,15.4,6.2,16.2,14,17.5,46.7,7.9,5.9-1.7,11-2.9,11.4-2.5.8.8,8.9,18.9,9.5,21.2.4,1.4-.3,2.1-4.1,4.2-5.6,3.1-7.2,4.4-7.2,5.5s1.7,6.5,3.9,13.2c2.1,6.8,4.1,13.6,4.3,15.2l.5,2.9-7.2,5c-4,2.8-9.8,7.5-12.9,10.4l-5.7,5.4-8.9.4-8.9.4-6.8,7.3q-9.4,10-4.1,10h4.3l4.6-6.9,4.6-6.9,24.6-.4c13.5-.2,25.7-.7,27.1-.9l2.5-.5-2.6,4.5c-1.4,2.5-3.3,5.7-4.3,7.2-1.9,3.1-2.1,3.9-.7,3.9s3.4-3.4,8.2-11.6c2.5-4.2,2.6-4.3,9.4-6.5,7.7-2.5,33.8-15.1,41.9-20.3l5.3-3.4-.4-3.2c-.2-1.8-.8-4.2-1.3-5.5l-.9-2.2-7.5.4c-7.3.3-14.2,2-16.2,4-1.3,1.2,1.2,9.7,4.1,13.9,2.2,3.3,2.5,4.2.7,2.7-1.6-1.3-5.5-9.5-6.1-12.8-.3-1.4-.7-2.5-1.1-2.5-2,0-10.9,8.4-15.3,14.6-2,2.7-3.7,4.5-3.9,4-.9-2.6,10.9-16.9,16.4-19.8,1.5-.8,2.7-1.9,2.7-2.4s-.8-4.5-1.8-8.9c-1-4.4-2.8-13.2-4-19.5-1.2-6.3-2.6-11.6-3.1-11.8-1.4-.5-12.7,4.9-13.2,6.4-.9,2.4,1.2,15.2,4.3,25.4,1.7,5.5,3.2,11.6,3.5,13.5.6,4.5.2,3.5-2.1-5-1-3.8-2.8-10.1-3.9-13.9-2.8-9.7-3.4-17.7-1.7-23.2,1.1-3.5,1.2-5,.5-8.7q-.9-4.9,6.1-10.2l2.5-1.9-2.5-5c-1.4-2.8-3-5.6-3.6-6.2-.6-.7-1.1-1.6-1.1-2s-2.6-2.6-5.8-4.8-5.8-4.1-5.8-4.2,1.2-1.5,2.7-3.1c3.3-3.4,8.9-13.3,8.9-15.6,0-3,2.9-5.7,6.1-5.7s8.6,4,8.6,6.4c0,2.3-2.3,6-6.4,10.3-2.1,2.3-3.5,4.1-3.1,4.1,1.3,0,7.9-7,10-10.7,1.9-3.3,1.9-3.5.7-5.8-4.7-9-8-10.8-13.4-7.5l-2.4,1.5-2.8-5.9c-4.7-9.9-4.9-12-5.8-54.6-.5-25.8-1-38.9-1.6-39.4-.6-.6-.7,9.3-.4,35.1.3,19.8.6,37,.8,38.3.4,2.4.4,2.4-3.2,2.3-3,0-3.8.3-5.1,2-1.4,1.8-1.4,2.2-.4,4.6,1.8,4.4,4.6,5.7,8.9,4.3,1.3-.4,1.7-.3,1.7.6s1.4,4.3,3.1,8.2c2.8,6.4,3.3,9.2,1.5,8.1-.4-.2-1.1-.2-1.6.1-.6.4-.5.9.5,1.6,2.1,1.5,1,5.1-3.4,12.1-3.7,5.8-6.2,8-6.8,6.1-.2-.5-.6-.7-1.1-.5-.5.3-.4.9.2,1.5.7.8.7,1.4-.1,2.3-2,2.4-19.6,15.5-28.8,21.5-7.9,5.1-9.9,6.1-14.9,7.1-7.3,1.4-13.5,1.4-16.8,0-4.4-1.8-8.8-10.5-6.6-13.2.5-.6,2.6-1.5,4.7-2,3.7-.9,8.4-3.9,8.4-5.4s-3.4-2.4-11.8-3.4c-6.5-.8-8.9-1.4-9.6-2.4-1.1-1.5-1.3-4.8-.3-6.3,1.1-1.7,13.9-2.9,20.9-1.9,4.7.7,6.9.7,9.8,0,5.8-1.5,5.1-3.1-1.6-3.6-4.6-.4-6.6-1.1-13-4.5-6.9-3.6-8-4-12-3.8-3.1.1-5-.2-6.3-1.1-1-.7-2.1-1.3-2.4-1.3-1.2,0-2.4,4.9-2,8.1.3,1.9.3,3.5.1,3.5-.9,0-4.6-8.8-8.5-20.3-5.5-16.2-6-21.8-3.5-34,2-9.3,1.8-9.1,9.1-10.1l4.1-.5-.5,2.5c-1,4.7-4.3,12.5-7.4,17.7-4.7,7.9-4.1,10.1,4.4,18.2,5.8,5.5,5.9,5.6,5.5,8.9l-.4,3.4,3-3.1,3-3.1-.4,3.3c-.2,2,0,3.3.4,3.3s.7-1.5.4-4c-.3-3.5-.8-4.3-3.8-7-8.2-7.3-10-9.2-11.2-11.7l-1.3-2.7,4.7-9.5c7.2-14.4,8.7-22.5,5.6-31.2-1.5-4.2-8.8-15.2-13.5-20.2-2.5-2.7-7.3-5.5-9.4-5.5s-1.1-.5-.8-2.1c.2-1.2.9-5.4,1.6-9.5,3.7-24,4.6-28.8,6.3-34.1,3.6-11.4,13-22.2,22.1-25.3,10.3-3.5,24.7-2.9,37.7,1.7,8.7,3,9.8,3.8,10.4,7.5.8,4.4,4.3,9.6,7.7,11.2,2.3,1.1,3.9,1.3,7.3.9,4.3-.5,4.5-.4,6.7,2.1,1.3,1.5,5.2,7.2,8.7,12.8,5.7,9,7.5,11.2,16.6,19.8,6.1,5.8,11.8,12,14.3,15.5l4.1,5.9-1.2,7.9c-1.5,9.7-2.2,35.3-1.2,41.7.4,2.6,1.8,7.1,3.2,10l2.4,5.3-8,8.1c-4.4,4.4-7.7,8.1-7.3,8.1s4.2-3.5,8.5-7.8q7.7-7.7,9.1-6.1c.8.9,1.4,2.9,1.4,4.6s.6,3.7,1.5,4.9l1.5,2-2.5,4c-10.3,16.5-15.4,22.5-25.6,30l-6.7,4.9,2,2.4c1.1,1.3,2.8,4.1,3.7,6.1l1.6,3.6,9.7-7c5.3-3.9,11-8.2,12.7-9.7l3.1-2.7.5,9.1c.3,5,.7,10.8,1.1,12.8.8,5.1,2.3,4.8,1.7-.4-2.8-24-3.1-34.7-.9-45.3,2.1-10.2,5.3-14.9,23.9-34.6l3-3.2-2.4-6.2c-4.6-12.1-4.9-22.8-.9-33.7,5.1-13.6,16.8-18.6,24.5-10.4,4,4.2,5.5,9.6,6.5,22.7,1.4,19.2.3,24.8-6.3,32.4-3.9,4.4-8.5,6.7-13.7,6.7s-4,0-4-2.1c0-1.2.5-3.9,1.1-6,1-3.8,1-5-.2-5s-19.7,20.8-23.6,25.9c-1.6,2.1-3.6,5.3-4.4,7-1.6,3.6-3.4,13.9-4.1,23.1l-.5,6.2,2.4-2.7c1.3-1.5,4.5-5.8,7.1-9.7,2.6-3.8,4.9-7.1,5.1-7.3,1-1-.7,16.1-2.1,21.4-2,7.1-1.6,8.3.6,1.7l1.6-4.8.4,3.5c1.5,12.9,2.4,17.3,5.2,25.7,1.7,5.2,5.4,14,8.1,19.7,4.1,8.5,5.2,10.3,6.4,9.9,2.9-.9,9.8-2,12.7-2s3-.2,3-.5-.9-1.7-2-3.1c-3.3-4.3-6.5-12.1-7.7-18.5-2.1-11.2-1.9-10.7-5.8-10.7-1.9,0-5.5.2-8.1.4l-4.6.4-.9-5c-1.3-7.1-1.2-18.9.2-24.3,1.3-5.1,7.3-20.8,7.8-20.3.2.2-.3,3.2-.9,6.7-.7,3.5-1,6.6-.8,6.8.2.2,1.3-.3,2.3-1.3,1-1,2-1.4,2.2-1.1.2.4,1.2,3.2,2.1,6.4,4,13.4,11.2,23.7,19.5,27.9,4.4,2.2,4.7,2.3,6.5,1.1,3.1-2,2.1-3.9-2.1-3.9-7.7,0-16.2-10.6-21.1-26.3-2.2-7-3.9-18.8-3.2-21,1.2-3.4,1.8-3.4,8,.8,7.2,4.9,17,9.7,19.8,9.7h2.1l-.3-17.5-.3-17.5,2.3-4.7c3.5-7.2,5.9-13.9,8.3-23.2l2.2-8.5v52c0,51.4,0,52,1.6,52s1.5-.5,1.6-48.5c0-37.6.3-50.3,1.1-56.8.6-4.6,1.4-8.7,1.7-9.3.3-.5,1.2,1.8,2.1,5.6,1.3,5.9,1.5,10.9,2,49.8.4,36.3.6,43.3,1.6,43.6.9.3,1-5.6.6-41.4-.4-36.7-.7-42.8-2.1-50.8-.9-5-1.5-10.2-1.5-11.6,0-3.6,5.5-17,6.4-15.5.3.6.7,36.4.9,79.4.2,65.9.4,80.3,1.5,91,1.6,15.9,5,43.4,5.9,47.4.7,3,3.4,6.6,4.3,5.7.3-.3-.4-7.7-1.5-16.6-2.9-23.2-4.9-44.1-5.8-60.5-.8-15.3-2.1-118.1-1.4-117.4.2.2,1.8,6.2,3.6,13.4,1.8,7.1,4,14.9,5.1,17.2,1.7,3.8,2.1,4.2,4,4,3.6-.4,3.9-2.1,1.6-9.6-1.1-3.8-3.7-15.7-5.6-26.5-1.9-10.8-3.7-20.2-3.9-20.9-.2-.6,3.4,2.7,8,7.4l8.4,8.6,7.6-.2c5.4-.1,7.8-.5,8.3-1.2,1-1.6.8-2.7-.6-4.2-1.1-1.1-2.2-1.2-6.9-.7l-5.6.5-8.1-8.4c-4.5-4.6-9.6-10.2-11.3-12.4-3-3.8-3.1-4.1-2.4-6.9.4-1.6.8-6.2.8-10.1,0-9.7-1.6-17.4-5.8-27.6-.6-1.3-.3-1.3,2,.4,1.5,1,5.6,4.8,9.3,8.3,5.8,5.6,7,6.4,9.4,6.4s3.8.8,7.3,3.6c2.5,2,4.9,4,5.5,4.4.8.6,1.4.3,2.5-1.1.8-1,1.5-2.5,1.5-3.2,0-1.5-4.3-5.2-11.2-9.6-2.5-1.6-9-6-14.3-9.6s-11.7-7.8-14.2-9.2c-5.2-2.9-5.5-2.1,3.4-7.9,12.6-8.2,14.1-17.8,4.4-28.1-4.4-4.7-4.6-5-3.7-7.3,1.5-4.2,1-9.6-1.1-13.9-1.9-3.7-7.2-9.5-10.9-12-.9-.6-2.3-2.9-3-5.1-.7-2.2-2.7-5.4-4.4-7.3l-3.1-3.3h-24.7c-27.2,0-26.9,0-32.8,5-3.6,3.1-7.1,9.3-8.5,15.1l-1,4.3.4-4.2c.7-7.2,4.3-13.8,9.9-17.9l3.1-2.3h-5.6c-5.6,0-5.6,0-9.3,3.5M470.4,9.3c-1.7.5-4.2,1.5-5.6,2.2-1.4.7-2.7,1.2-2.9,1-.6-.6,3.9-3.5,6.7-4.2,3.8-1,5.5-.1,1.8,1M354.2,13.5c-1.5,1-3.3,1-2.7,0,.3-.4,1.2-.8,2.2-.8,1.4,0,1.5.2.5.8M474.3,16.2c-2.5,1-5.3,2-6.2,2.3l-1.5.6,1.5-1.3c2.3-2,5.9-3.4,8.5-3.4,1.9,0,1.5.3-2.3,1.8M479,19.6c-.8.3-2.6.8-3.9,1.1l-2.3.5,2.3-1.1c1.3-.6,3-1.1,3.9-1.1h1.5s-1.5.6-1.5.6M434.3,24.5c-.2.6-.4.4-.5-.5,0-.8.1-1.3.4-1s.3.9,0,1.5M335.8,28.6c-1.4.6-3.6,1.9-4.9,2.8-1.6,1.1-2.2,1.3-2,.6.6-1.7,5.3-4.6,7.5-4.5,1.9,0,1.9,0-.6,1.1M400.9,36.4c4.7.6,13.7,2.4,20.1,4l11.6,2.8-8.5,1.8c-4.7,1-10.5,2.5-13.1,3.5-6.1,2.2-17.1,7.8-21.7,11.1l-3.7,2.6-3.5-2.4c-4.8-3.3-15.5-8-22-9.5l-5.3-1.3,2.6-1.6c3.3-2,13.8-7.1,17-8.2,4.8-1.7,1.4-1.8-4.1,0-6.3,1.9-12.1,4.7-15.8,7.4-2.2,1.6-3.1,1.8-10,1.7-7.7-.1-15.3,1.2-16.3,2.7-.3.6,0,.6,1,.2.8-.3,5.8-.8,11.1-1l9.7-.3-3.6,2.3c-4.6,2.9-13.7,11.6-16.4,15.5l-2.1,3-7.1.5c-11.1.8-19.2,3.4-26.4,8.4q-2.8,1.9,4.8-7.3c11.5-14,31.7-27.6,48.9-33,15.2-4.8,31.6-5.7,52.9-2.8M497.5,40.1c1.5.6,2.7,1.2,2.7,1.4s-3.4.1-7.5,0c-4.8-.2-9.6.2-13.3,1-3.2.7-5.9,1.1-6,.9-.3-.3,5.1-2.4,9-3.5,4.1-1.1,12.1-1,15.1.2M495.9,45.7c.6.3-2.8.6-7.7.6s-8.8,0-8.8-.2,1.1-.5,2.5-.9c2.7-.8,12.2-.5,14,.5M449.7,49.7c1.2.7,1.2.9.3,1.1-14.2,4.1-19.6,6-26.7,9.4-16.7,8.1-29,17.1-39.4,28.7-3.2,3.6-5.9,6.5-6.1,6.6-.7.4-10.5-8.2-10.5-9.2,0-1.6,9.9-11.1,17.7-17,12.3-9.3,32.8-18.2,46.8-20.3,5.6-.9,15.9-.5,17.9.6M362.3,53.5c5.9,2.1,20.5,9.4,20.5,10.3s-1.3,1.5-2.9,2.7c-1.6,1.2-6.1,5.4-10,9.4l-7.1,7.2-8.7-4.3c-4.8-2.3-11.8-5.1-15.5-6-3.7-1-6.8-2.1-6.8-2.4s4.3-5,9.6-10.2c10.9-10.9,10-10.5,20.9-6.7M490.8,53.4l1.7,1.2v60.1c0,40.7-.3,60.9-.8,62.5-.9,2.7-2,3.3-4.3,2.5-1.4-.5-1.7-1.4-2.1-7.7-.3-3.9-.5-32.4-.5-63.4v-56.4c0,0,2.1,0,2.1,0,1.2,0,2.9.5,3.9,1.2M445.4,71.5c3.6,3.9,6.6,7.3,6.7,7.6s-2.8,2-6.6,3.9c-7.3,3.6-18,10.3-23.1,14.5-1.8,1.4-3.6,2.6-4,2.6s-1.3-.9-2.1-2c-.7-1.1-3.6-4.6-6.4-7.8l-5-5.8,2.5-2c11.2-9,26-17.9,29.6-17.9s3.8,2.3,8.2,7M480.1,80c-1.4,8.3-5.2,15.9-15.2,30.7-5.2,7.8-10.4,15.8-11.4,17.9-1.6,3.2-2.3,3.9-4.7,4.3-1.5.3-3,.4-3.2.1-.5-.5,1.9-8.7,3.4-11.7.7-1.3,5.3-8.6,10.3-16.3,10.2-15.6,17.7-28.8,19.8-35l1.4-4.1.2,4.5c.1,2.5-.2,6.8-.6,9.5M502,124.8c0,6.6-.3,12.7-.7,13.7-.5,1.1-.7-2.9-.7-12s-.2-15.6-.5-17.9l-.5-4.2,1.2,4.2c.8,2.9,1.2,8.1,1.2,16.2M453.5,137c-11.7,5.6-19.5,19.3-21.6,38.2-.4,3.6-.8,4.4-1.9,4.4-2.1,0-2.6-1.3-.8-1.9,2-.6,2-2.3,0-3.7l-1.6-1.1,1.6-.5c1.9-.6,2.2-3.3.5-4-1.7-.6-1.4-1.8.3-1.8s2.6-1.2,1.1-2.7c-1.2-1.2-1.1-1.3.4-1.7,2-.5,2.1-1.5.3-2.5-.7-.4-1-.8-.5-.8.4,0,1.5-.5,2.3-1.1,1.5-1.1,1.5-1.1,0-2.3-1.4-1-1.4-1.2-.2-1.2,1.4,0,3.6-3.1,2.2-3.1s-.8-.3-.8-.8.7-.8,1.6-.8,1.5-.4,1.2-1.6c-.7-2.8,9.2-11,15.2-12.4,5.1-1.2,5.4-.7.9,1.5M334.1,137.5c-7.7,1.6-14,4.3-16.8,7-2.1,2.1-2.5,3.1-2.5,6,0,6.8,5.9,10.7,9.5,6.2,3.8-4.7,8-8.3,11.7-10,9.1-4.2,21.9-4.3,33.6-.4,5.8,2,6.3,2,4.7.6-2.8-2.3-12.1-6.4-18.6-8.3-7.6-2.1-15.3-2.6-21.6-1.2M335.1,159.5c-1.8.9-3.7,2.4-4.2,3.5-.5,1.1-1.3,1.8-1.9,1.6-1.4-.5-1.3,1.1.2,3.2,1.1,1.5,1,2.2-.8,7.5-1.1,3.2-2,6.1-2,6.4s1.7-1.3,3.8-3.7l3.8-4.2-.4,2.7c-.2,1.5-.7,3.7-1.1,4.9-.4,1.2-.7,2.5-.7,2.8s3.2.6,7,.7c10.1.3,13.3-1,21.3-8.4,5.5-5.1,7.5-6.4,12.9-8.4,3.5-1.3,7-2.9,7.8-3.6,1.4-1.1,1.4-1.2-.8-.9-1.3.2-4.8.8-7.9,1.4-4.7.9-6.5.9-11.2,0-12.2-2.3-20.5-2-27.8,1.1-2.7,1.1-.6-1.6,2.7-3.4,5-2.8,11.8-2.9,21.4-.5,4.4,1.1,8.2,1.9,8.4,1.6.6-.6.2-.8-6-3.1-8-2.9-19.7-3.5-24.5-1.2M284.3,160.8c3.4,2.2,9.6,8,9.6,8.9s-1,0-2.1-.5c-3.6-1.9-6.7-2.1-9.8-.8-1.6.7-3,1.1-3.1,1-.4-.5-2.1-5.8-2.7-8.1-.6-2.5-.6-2.5,2.2-2.5s4,.8,5.9,2M465.6,160c-1.9.8-3,2.1-4.6,5.5-1.8,3.9-2.1,5.7-2.4,14.1-.2,7.7,0,10,.9,11.2,1.1,1.5,1.2,1.4,2.1-1.9,1.1-4.2,5.2-8.5,8-8.5s2.9.8,4.1,1.8c3.3,2.8,2.9,6.9-.9,10.4-2.2,2-2.7,2.9-2.1,3.8,1.5,2.4,3.1,2.8,5.2,1.4l2-1.3v-11.5c0-6.3-.4-13.3-.8-15.4-1.7-7.8-6.4-11.8-11.4-9.7M291.7,171.3c2.7.9,5.6,3,5,3.5-.2.2-1.5-.3-2.9-1-1.7-.9-4.4-1.4-8.2-1.4-3.1,0-5.6-.3-5.6-.6,0-1.4,7.9-1.8,11.7-.5M358.8,173.2c.8.7-4.6,6.2-7.7,8l-3.4,1.9,1.3-1.9c.8-1.1,1.3-3.3,1.3-5.6v-3.8l4.1.5c2.2.3,4.2.7,4.4.8M339.2,180.9c1.7,1.6,2.4,2.6,1.6,2.3-.8-.3-2.3-.5-3.3-.5-1.6,0-1.9-.4-1.9-2.3s.1-2.3.3-2.3,1.6,1.3,3.3,2.9M291.3,187.2c-.8.8-3.4-.7-4.2-2.5-1.7-3.8-.9-4.3,1.8-1.1,1.5,1.8,2.6,3.4,2.4,3.6M431.5,185.3c-.3.3-1.2.4-1.9.1-1.2-.5-1.2-.6,0-1.5,1-.7,1.5-.8,1.9-.1.3.5.3,1.2,0,1.5M369,184.4c-.5.5-.9,1.2-.9,1.5,0,.8,9.4,5.3,10.1,4.9,1.1-.7.8-2.2-.7-3.5-4.5-3.8-6.8-4.6-8.5-2.9M282.3,186.9c0,.2-.5.5-1.2.8-.7.3-1,.1-.7-.3.5-.7,1.9-1.1,1.9-.5M432.2,189.3c0,.6-.5,1.2-1.1,1.2-1.5,0-2.2-1.3-1-1.8,1.7-.7,2.1-.5,2.1.6M349.6,190.3c0,.3,1.1,1.1,2.5,1.7,1.4.6,4.4,3.7,6.9,7,6.5,8.6,11.5,11.4,18.1,10.5,4.6-.7,4.3-1.6-.5-1.8-4.9-.2-9.7-2.6-11.4-5.7-3.7-6.8-5.7-9.2-8.6-10.7-3.1-1.6-7-2.1-7-1M433,193.9c0,1.3-.7,1.4-2.7.7-1-.4-1.1-.6-.3-1.1,1.6-1,3-.8,3,.5M434.1,198.6c.7,1.7.7,1.7-1.5,1.3-2.1-.4-2.5-1.4-1-2,1.6-.7,1.9-.6,2.4.7M436.1,203.8c0,.8-2.9.7-3.4-.1-.2-.4.2-1.1.9-1.5,1.3-.7,2.5,0,2.5,1.6M437.6,207.8c1,1.3.8,1.4-1.1.7-.7-.3-1.2-.8-1.2-1.2,0-1.1,1.3-.8,2.3.5M440.5,212.9c1.1,1.8.4,2-1.6.5-1.6-1.2-1.7-2-.4-2s1.4.7,1.9,1.5M489.8,239.7c-.6.6-10.7-4.4-17-8.5-5.7-3.7-6-4-4.2-4.7,7.6-2.9,12.2-5.8,16.5-10.2l4.7-4.8.2,13.9c.1,7.7,0,14.1-.2,14.3M383.6,214.4c1.8.9,2,4,.3,5.4-1.7,1.4-5,.3-5.8-2-1.3-3.4,1.8-5.3,5.5-3.3M300.3,221.1c-.5,1.2,2.3,4.1,4,4.1.4,0,2.8,1.1,5.4,2.4,4.1,2.1,4.9,2.2,6.8,1.4,1.2-.5,1.9-1.1,1.6-1.4-.4-.3-1.1-1.4-1.6-2.6-1.3-2.9-6.4-5.1-11.6-5.2-3.1,0-4.2.3-4.6,1.2M300.7,260.4c-.6,1.9-1,2.4-1.5,1.7-.9-1.5-.8-3.5.3-4,1.8-.7,2-.5,1.1,2.2M387.6,267.7c3.3,2.3,5.9,4.5,6,4.8,0,1.1-15.4,8.4-23.9,11.3-8.7,3-23.1,6.7-23.6,6.1-.2-.2,4.7-3.8,10.7-8.1,6-4.3,14-10.3,17.6-13.2,3.6-3,6.8-5.3,7-5.2.2,0,3.1,2.1,6.4,4.4M402.4,287.5c.9,4.9,3.8,13.1,4.1,11.7.2-.7-.6-4.3-1.7-7.9-2.1-6.8-3.3-8.8-2.4-3.8M437.6,290.4c-6.6,2-6.1.5-4.1,12.9,1.3,8,6.3,29.9,7.2,31.6.5.8.7.7,1.1-.6.6-2,8.5-7.7,14.7-10.7,2.5-1.2,4.8-2.3,4.9-2.4.2-.1,0-.6-.5-1.2-1.6-2-8.1-15.7-11-23.4-3.4-9-3.4-9-12.3-6.3M420.2,296c-12.8,4.5-13.7,4.9-13.2,5.6.3.6,7.1,32.1,8,37.4.2,1.2.6,2.3.9,2.5.3.2,2.4-.4,4.7-1.3,3.3-1.3,6-1.6,12.5-1.7,7.1,0,8.2-.3,7.8-1.2-3.7-8.5-7.7-24.4-9.6-37.7-.5-3.5-1.1-6.5-1.4-6.6-.3-.1-4.7,1.2-9.8,3M391.3,298.1c0,8.6-.3,9.2-5.9,12-2.7,1.4-5.3,2.3-5.7,2.1-.4-.2-.9-1.5-1.2-2.8-.4-1.8,0-2.9,1.4-4.9,3.2-4.2,9.9-11.3,10.6-11.3s.7,2.2.7,4.9M486.6,319.8c-.8,2,5.1,9.9,13.1,17.8,8.1,7.9,19.5,17.2,20.3,16.4.2-.2-5.4-6.4-12.6-13.7-7.2-7.3-14.2-15.1-15.7-17.4-2.7-4.2-4.3-5.2-5.1-3.2M443.8,347c0,3.8,3.2,9,6.5,10.6,3.7,1.8,10.1,1.6,20.9-.6,9.4-1.9,11.5-2.8,10-4.3-.6-.6-2.2-.5-5.4.3-2.5.6-6.3,1.4-8.5,1.7-4.9.6-8.3-.8-16-6.5-2.9-2.2-5.8-4-6.4-4s-1.1,1-1.1,2.8M441.1,361.9c1.9,1.9,25.1,9.1,25.1,7.8s-23.5-8.5-25.1-8.5-.3.3,0,.7M500.6,362.1c-15,1.6-29.3,9.3-41.8,22.6-3.6,3.8-6.5,7.2-6.5,7.6s2,.7,4.4.7h4.4l6-5.7c7.1-6.8,13.4-10.6,22.8-13.9,6.7-2.3,7.5-2.4,19-2.4,13.7,0,17.9.9,31.1,7.1,4.1,1.9,7.8,3.3,8.1,3,.8-.8-5.2-6.6-10.6-10.1-10.9-7.1-23.7-10.2-36.7-8.8M427.6,365.3c1.6,1.6,27,12.2,28.6,11.9,1.7-.4-4.6-3.9-14.8-8.3-9.5-4.1-15.8-5.8-13.8-3.6M500.2,381.6c-4.9,1.4-8,3.1-11.2,6.3-4.6,4.6-4,4.9,6.9,3,7.9-1.4,15.9-1.1,21.2.8,3.7,1.3,10.9,1.7,10.9.5s-3.9-5.4-7.2-7.2c-6.9-4-13.9-5.2-20.7-3.3"/>
14 |     </g>
15 |     <g>
16 |         <path d="M301.4,474.1v-55.1h8.7l15.8,36.3h.5l15.8-36.3h8.6v55.1h-5.3v-49.9h-.5l-16,36.4h-5.9l-16-36.4h-.5v49.9h-5.2Z"/>
17 |         <path d="M362.8,452.7c0-5.6.7-10,2-13.2,1.3-3.3,3.4-5.6,6.2-6.9,2.8-1.4,6.4-2.1,10.8-2.1s8,.7,10.8,2.1c2.8,1.4,4.8,3.7,6.2,6.9,1.4,3.3,2.1,7.7,2.1,13.2s-.7,10-2.1,13.2c-1.4,3.3-3.4,5.6-6.2,6.9-2.8,1.4-6.4,2.1-10.8,2.1s-8-.7-10.8-2.1c-2.8-1.4-4.8-3.7-6.2-6.9-1.3-3.3-2-7.7-2-13.2ZM395.5,452.7c0-4.6-.4-8.1-1.3-10.7-.8-2.6-2.3-4.4-4.2-5.5s-4.7-1.7-8.2-1.7-6.3.6-8.3,1.7c-2,1.1-3.3,3-4.2,5.5-.8,2.6-1.2,6.1-1.2,10.7s.4,8.1,1.3,10.7c.8,2.6,2.2,4.4,4.2,5.5,2,1.1,4.7,1.7,8.2,1.7s6.4-.6,8.3-1.7c2-1.1,3.3-2.9,4.2-5.5.8-2.5,1.2-6.1,1.2-10.7Z"/>
18 |         <path d="M418.3,469.3h24.7v4.8h-29.9v-55.1h5.2v50.2Z"/>
19 |         <path d="M451.3,419.2c0-1.4.2-2.3.7-2.7.4-.4,1.3-.6,2.6-.6s2,.2,2.4.6c.4.4.7,1.3.7,2.7s-.2,2.3-.7,2.7c-.5.4-1.3.6-2.4.6s-2.1-.2-2.6-.6c-.5-.4-.7-1.3-.7-2.7ZM451.9,474.1v-42.9h5.1v42.9h-5.1Z"/>
20 |         <path d="M505,444.3v29.8h-5.1v-27.5c0-2.8-.3-5-.8-6.7-.5-1.7-1.5-2.9-2.9-3.7-1.4-.8-3.4-1.2-6-1.2s-6.2.7-8.6,2-4,3.9-5,7.5v29.6h-5v-42.9h5v6.8c.9-2.6,2.7-4.6,5.3-5.8,2.6-1.2,5.6-1.8,8.8-1.8,5.2,0,8.8,1.2,11,3.5,2.1,2.4,3.2,5.8,3.2,10.3Z"/>
21 |         <path d="M553.8,471.3c0,6-1.7,10.7-5,13.9-3.4,3.2-8.4,4.8-15.1,4.8s-7.3-.3-10-1v-4.4c2.8.6,6.1,1,10,1s9-1.2,11.5-3.7c2.5-2.4,3.7-6.3,3.7-11.5v-5c-1,2.9-2.6,5-4.9,6.5-2.3,1.5-5.6,2.2-9.8,2.2s-7.8-.8-10.3-2.5c-2.5-1.7-4.3-4.1-5.3-7.2-1-3.1-1.5-7.2-1.5-12.1s1.3-12.3,3.8-16.1c2.6-3.8,7-5.7,13.3-5.7s7.5.8,9.8,2.5c2.3,1.7,4,4,4.9,6.9v-8.7h5v40ZM549,452.3c0-5.7-1.1-10-3.2-13-2.1-3-5.8-4.5-11-4.5s-6.1.7-7.9,2.1c-1.8,1.4-3.1,3.4-3.7,5.9-.6,2.5-1,5.7-1,9.4s.3,7.1,1,9.6c.7,2.5,1.9,4.4,3.7,5.8,1.8,1.4,4.4,2.1,7.8,2.1,5.2,0,8.9-1.5,11-4.4,2.1-3,3.1-7.3,3.1-13Z"/>
22 |     </g>
23 | </svg>
```

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

```go
  1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //   http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | //
 15 | // Repository: https://github.com/gojue/moling
 16 | 
 17 | // Package services provides a set of services for the MoLing application.
 18 | package browser
 19 | 
 20 | import (
 21 | 	"context"
 22 | 	"encoding/json"
 23 | 	"fmt"
 24 | 	"math/rand"
 25 | 	"os"
 26 | 	"path/filepath"
 27 | 	"strings"
 28 | 	"time"
 29 | 
 30 | 	"github.com/chromedp/chromedp"
 31 | 	"github.com/mark3labs/mcp-go/mcp"
 32 | 	"github.com/rs/zerolog"
 33 | 
 34 | 	"github.com/gojue/moling/pkg/comm"
 35 | 	"github.com/gojue/moling/pkg/config"
 36 | 	"github.com/gojue/moling/pkg/services/abstract"
 37 | 	"github.com/gojue/moling/pkg/utils"
 38 | )
 39 | 
 40 | const (
 41 | 	BrowserDataPath                         = "browser" // Path to store browser data
 42 | 	BrowserServerName comm.MoLingServerType = "Browser"
 43 | )
 44 | 
 45 | // BrowserServer represents the configuration for the browser service.
 46 | type BrowserServer struct {
 47 | 	abstract.MLService
 48 | 	config       *BrowserConfig
 49 | 	name         string // The name of the service
 50 | 	cancelAlloc  context.CancelFunc
 51 | 	cancelChrome context.CancelFunc
 52 | }
 53 | 
 54 | // NewBrowserServer creates a new BrowserServer instance with the given context and configuration.
 55 | func NewBrowserServer(ctx context.Context) (abstract.Service, error) {
 56 | 	bc := NewBrowserConfig()
 57 | 	globalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)
 58 | 	bc.BrowserDataPath = filepath.Join(globalConf.BasePath, BrowserDataPath)
 59 | 	bc.DataPath = filepath.Join(globalConf.BasePath, "data")
 60 | 	logger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)
 61 | 	if !ok {
 62 | 		return nil, fmt.Errorf("BrowserServer: invalid logger type: %T", ctx.Value(comm.MoLingLoggerKey))
 63 | 	}
 64 | 	loggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
 65 | 		e.Str("Service", string(BrowserServerName))
 66 | 	})
 67 | 	bs := &BrowserServer{
 68 | 		MLService: abstract.NewMLService(ctx, logger.Hook(loggerNameHook), globalConf),
 69 | 		config:    bc,
 70 | 	}
 71 | 
 72 | 	err := bs.InitResources()
 73 | 	if err != nil {
 74 | 		return nil, err
 75 | 	}
 76 | 
 77 | 	return bs, nil
 78 | }
 79 | 
 80 | // Init initializes the browser server by creating a new context.
 81 | func (bs *BrowserServer) Init() error {
 82 | 	// Initialize the browser server
 83 | 	err := bs.initBrowser(bs.config.BrowserDataPath)
 84 | 	if err != nil {
 85 | 		return fmt.Errorf("failed to initialize browser: %w", err)
 86 | 	}
 87 | 	err = utils.CreateDirectory(bs.config.DataPath)
 88 | 	if err != nil {
 89 | 		return fmt.Errorf("failed to create data directory: %w", err)
 90 | 	}
 91 | 
 92 | 	// Create a new context for the browser
 93 | 	opts := append(
 94 | 		chromedp.DefaultExecAllocatorOptions[:],
 95 | 		chromedp.UserAgent(bs.config.UserAgent),
 96 | 		chromedp.Flag("lang", bs.config.DefaultLanguage),
 97 | 		chromedp.Flag("disable-blink-features", "AutomationControlled"),
 98 | 		chromedp.Flag("enable-automation", false),
 99 | 		chromedp.Flag("disable-features", "Translate"),
100 | 		chromedp.Flag("hide-scrollbars", false),
101 | 		chromedp.Flag("mute-audio", true),
102 | 		//chromedp.Flag("no-sandbox", true),
103 | 		chromedp.Flag("disable-infobars", true),
104 | 		chromedp.Flag("disable-extensions", true),
105 | 		chromedp.Flag("CommandLineFlagSecurityWarningsEnabled", false),
106 | 		chromedp.Flag("disable-notifications", true),
107 | 		chromedp.Flag("disable-dev-shm-usage", true),
108 | 		chromedp.Flag("autoplay-policy", "user-gesture-required"),
109 | 		chromedp.CombinedOutput(bs.Logger),
110 | 		// (1920, 1080), (1366, 768), (1440, 900), (1280, 800)
111 | 		chromedp.WindowSize(1280, 800),
112 | 		chromedp.UserDataDir(bs.config.BrowserDataPath),
113 | 		chromedp.IgnoreCertErrors,
114 | 	)
115 | 
116 | 	// headless mode
117 | 	if bs.config.Headless {
118 | 		opts = append(opts, chromedp.Flag("headless", true))
119 | 		opts = append(opts, chromedp.Flag("disable-gpu", true))
120 | 		opts = append(opts, chromedp.Flag("disable-webgl", true))
121 | 	}
122 | 
123 | 	bs.Context, bs.cancelAlloc = chromedp.NewExecAllocator(context.Background(), opts...)
124 | 
125 | 	bs.Context, bs.cancelChrome = chromedp.NewContext(bs.Context,
126 | 		chromedp.WithErrorf(bs.Logger.Error().Msgf),
127 | 		chromedp.WithDebugf(bs.Logger.Debug().Msgf),
128 | 	)
129 | 
130 | 	pe := abstract.PromptEntry{
131 | 		PromptVar: mcp.Prompt{
132 | 			Name:        "browser_prompt",
133 | 			Description: "Get the relevant functions and prompts of the Browser MCP Server",
134 | 			//Arguments:   make([]mcp.PromptArgument, 0),
135 | 		},
136 | 		HandlerFunc: bs.handlePrompt,
137 | 	}
138 | 	bs.AddPrompt(pe)
139 | 	bs.AddTool(mcp.NewTool(
140 | 		"browser_navigate",
141 | 		mcp.WithDescription("Navigate to a URL"),
142 | 		mcp.WithString("url",
143 | 			mcp.Description("URL to navigate to"),
144 | 			mcp.Required(),
145 | 		),
146 | 	), bs.handleNavigate)
147 | 	bs.AddTool(mcp.NewTool(
148 | 		"browser_screenshot",
149 | 		mcp.WithDescription("Take a screenshot of the current page or a specific element"),
150 | 		mcp.WithString("name",
151 | 			mcp.Description("Name for the screenshot"),
152 | 			mcp.Required(),
153 | 		),
154 | 		mcp.WithString("selector",
155 | 			mcp.Description("CSS selector for element to screenshot"),
156 | 		),
157 | 		mcp.WithNumber("width",
158 | 			mcp.Description("Width in pixels (default: 1700)"),
159 | 		),
160 | 		mcp.WithNumber("height",
161 | 			mcp.Description("Height in pixels (default: 1100)"),
162 | 		),
163 | 	), bs.handleScreenshot)
164 | 	bs.AddTool(mcp.NewTool(
165 | 		"browser_click",
166 | 		mcp.WithDescription("Click an element on the page"),
167 | 		mcp.WithString("selector",
168 | 			mcp.Description("CSS selector for element to click"),
169 | 			mcp.Required(),
170 | 		),
171 | 	), bs.handleClick)
172 | 	bs.AddTool(mcp.NewTool(
173 | 		"browser_fill",
174 | 		mcp.WithDescription("Fill out an input field"),
175 | 		mcp.WithString("selector",
176 | 			mcp.Description("CSS selector for input field"),
177 | 			mcp.Required(),
178 | 		),
179 | 		mcp.WithString("value",
180 | 			mcp.Description("Value to fill"),
181 | 			mcp.Required(),
182 | 		),
183 | 	), bs.handleFill)
184 | 	bs.AddTool(mcp.NewTool(
185 | 		"browser_select",
186 | 		mcp.WithDescription("Select an element on the page with Select tag"),
187 | 		mcp.WithString("selector",
188 | 			mcp.Description("CSS selector for element to select"),
189 | 			mcp.Required(),
190 | 		),
191 | 		mcp.WithString("value",
192 | 			mcp.Description("Value to select"),
193 | 			mcp.Required(),
194 | 		),
195 | 	), bs.handleSelect)
196 | 	bs.AddTool(mcp.NewTool(
197 | 		"browser_hover",
198 | 		mcp.WithDescription("Hover an element on the page"),
199 | 		mcp.WithString("selector",
200 | 			mcp.Description("CSS selector for element to hover"),
201 | 			mcp.Required(),
202 | 		),
203 | 	), bs.handleHover)
204 | 	bs.AddTool(mcp.NewTool(
205 | 		"browser_evaluate",
206 | 		mcp.WithDescription("Execute JavaScript in the browser console"),
207 | 		mcp.WithString("script",
208 | 			mcp.Description("JavaScript code to execute"),
209 | 			mcp.Required(),
210 | 		),
211 | 	), bs.handleEvaluate)
212 | 
213 | 	bs.AddTool(mcp.NewTool(
214 | 		"browser_debug_enable",
215 | 		mcp.WithDescription("Enable JavaScript debugging"),
216 | 		mcp.WithBoolean("enabled",
217 | 			mcp.Description("Enable or disable debugging"),
218 | 			mcp.Required(),
219 | 		),
220 | 	), bs.handleDebugEnable)
221 | 
222 | 	bs.AddTool(mcp.NewTool(
223 | 		"browser_set_breakpoint",
224 | 		mcp.WithDescription("Set a JavaScript breakpoint"),
225 | 		mcp.WithString("url",
226 | 			mcp.Description("URL of the script"),
227 | 			mcp.Required(),
228 | 		),
229 | 		mcp.WithNumber("line",
230 | 			mcp.Description("Line number"),
231 | 			mcp.Required(),
232 | 		),
233 | 		mcp.WithNumber("column",
234 | 			mcp.Description("Column number (optional)"),
235 | 		),
236 | 		mcp.WithString("condition",
237 | 			mcp.Description("Breakpoint condition (optional)"),
238 | 		),
239 | 	), bs.handleSetBreakpoint)
240 | 
241 | 	bs.AddTool(mcp.NewTool(
242 | 		"browser_remove_breakpoint",
243 | 		mcp.WithDescription("Remove a JavaScript breakpoint"),
244 | 		mcp.WithString("breakpointId",
245 | 			mcp.Description("Breakpoint ID to remove"),
246 | 			mcp.Required(),
247 | 		),
248 | 	), bs.handleRemoveBreakpoint)
249 | 
250 | 	bs.AddTool(mcp.NewTool(
251 | 		"browser_pause",
252 | 		mcp.WithDescription("Pause JavaScript execution"),
253 | 	), bs.handlePause)
254 | 
255 | 	bs.AddTool(mcp.NewTool(
256 | 		"browser_resume",
257 | 		mcp.WithDescription("Resume JavaScript execution"),
258 | 	), bs.handleResume)
259 | 
260 | 	bs.AddTool(mcp.NewTool(
261 | 		"browser_get_callstack",
262 | 		mcp.WithDescription("Get current call stack when paused"),
263 | 	), bs.handleGetCallstack)
264 | 	return nil
265 | }
266 | 
267 | // init initializes the browser server by creating the user data directory.
268 | func (bs *BrowserServer) initBrowser(userDataDir string) error {
269 | 	_, err := os.Stat(userDataDir)
270 | 	if err != nil && !os.IsNotExist(err) {
271 | 		return fmt.Errorf("failed to stat user data directory: %w", err)
272 | 	}
273 | 
274 | 	// Check if the directory exists, if it does, we can reuse it
275 | 	if err == nil {
276 | 		//  判断浏览器运行锁
277 | 		singletonLock := filepath.Join(userDataDir, "SingletonLock")
278 | 		_, err = os.Stat(singletonLock)
279 | 		if err == nil {
280 | 			bs.Logger.Debug().Msg("Browser is already running, removing SingletonLock")
281 | 			err = os.RemoveAll(singletonLock)
282 | 			if err != nil {
283 | 				bs.Logger.Error().Str("Lock", singletonLock).Msgf("Browser can't work due to failed removal of SingletonLock: %s", err.Error())
284 | 			}
285 | 		}
286 | 		return nil
287 | 	}
288 | 	// Create the directory
289 | 	err = os.MkdirAll(userDataDir, 0755)
290 | 	if err != nil {
291 | 		return fmt.Errorf("failed to create user data directory: %w", err)
292 | 	}
293 | 	return nil
294 | }
295 | 
296 | func (bs *BrowserServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
297 | 	// 处理浏览器提示
298 | 	return &mcp.GetPromptResult{
299 | 		Description: "",
300 | 		Messages: []mcp.PromptMessage{
301 | 			{
302 | 				Role: mcp.RoleUser,
303 | 				Content: mcp.TextContent{
304 | 					Type: "text",
305 | 					Text: bs.config.prompt,
306 | 				},
307 | 			},
308 | 		},
309 | 	}, nil
310 | }
311 | 
312 | // handleNavigate handles the navigation action.
313 | func (bs *BrowserServer) handleNavigate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
314 | 	args := request.GetArguments()
315 | 	url, ok := args["url"].(string)
316 | 	if !ok {
317 | 		return nil, fmt.Errorf("url must be a string")
318 | 	}
319 | 
320 | 	err := chromedp.Run(bs.Context, chromedp.Navigate(url))
321 | 	if err != nil {
322 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to navigate: %s", err.Error())), nil
323 | 	}
324 | 	return mcp.NewToolResultText(fmt.Sprintf("Navigated to %s", url)), nil
325 | }
326 | 
327 | // handleScreenshot handles the screenshot action.
328 | func (bs *BrowserServer) handleScreenshot(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
329 | 	args := request.GetArguments()
330 | 	name, ok := args["name"].(string)
331 | 	if !ok {
332 | 		return mcp.NewToolResultError("name must be a string"), nil
333 | 	}
334 | 	selector, _ := args["selector"].(string)
335 | 	width, _ := args["width"].(int)
336 | 	height, _ := args["height"].(int)
337 | 	if width == 0 {
338 | 		width = 1280
339 | 	}
340 | 	if height == 0 {
341 | 		height = 800
342 | 	}
343 | 	var buf []byte
344 | 	var err error
345 | 	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
346 | 	defer cancelFunc()
347 | 	if selector == "" {
348 | 		err = chromedp.Run(runCtx, chromedp.FullScreenshot(&buf, 90))
349 | 	} else {
350 | 		err = chromedp.Run(bs.Context, chromedp.Screenshot(selector, &buf, chromedp.NodeVisible))
351 | 	}
352 | 	if err != nil {
353 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to take screenshot: %s", err.Error())), nil
354 | 	}
355 | 
356 | 	newName := filepath.Join(bs.config.DataPath, fmt.Sprintf("%s_%d.png", strings.TrimRight(name, ".png"), rand.Int()))
357 | 	err = os.WriteFile(newName, buf, 0644)
358 | 	if err != nil {
359 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to save screenshot: %s", err.Error())), nil
360 | 	}
361 | 	return mcp.NewToolResultText(fmt.Sprintf("Screenshot saved to:%s", newName)), nil
362 | }
363 | 
364 | // handleClick handles the click action on a specified element.
365 | func (bs *BrowserServer) handleClick(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
366 | 	args := request.GetArguments()
367 | 	selector, ok := args["selector"].(string)
368 | 	if !ok {
369 | 		return mcp.NewToolResultError(fmt.Sprintf("selector must be a string:%v", selector)), nil
370 | 	}
371 | 	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
372 | 	defer cancelFunc()
373 | 	err := chromedp.Run(runCtx,
374 | 		chromedp.WaitReady("body", chromedp.ByQuery), // 等待页面就绪
375 | 		chromedp.WaitVisible(selector, chromedp.ByQuery),
376 | 		chromedp.Click(selector, chromedp.NodeVisible),
377 | 	)
378 | 	if err != nil {
379 | 		return mcp.NewToolResultError(fmt.Errorf("failed to click element: %s", err.Error()).Error()), nil
380 | 	}
381 | 	return mcp.NewToolResultText(fmt.Sprintf("Clicked element %s", selector)), nil
382 | }
383 | 
384 | // handleFill handles the fill action on a specified input field.
385 | func (bs *BrowserServer) handleFill(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
386 | 	args := request.GetArguments()
387 | 	selector, ok := args["selector"].(string)
388 | 	if !ok {
389 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to fill selector:%v", args["selector"])), nil
390 | 	}
391 | 
392 | 	value, ok := args["value"].(string)
393 | 	if !ok {
394 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to fill input field: %v, selector:%v", args["value"], selector)), nil
395 | 	}
396 | 
397 | 	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
398 | 	defer cancelFunc()
399 | 	err := chromedp.Run(runCtx, chromedp.SendKeys(selector, value, chromedp.NodeVisible))
400 | 	if err != nil {
401 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to fill input field: %s", err.Error())), nil
402 | 	}
403 | 	return mcp.NewToolResultText(fmt.Sprintf("Filled input %s with value %s", selector, value)), nil
404 | }
405 | 
406 | func (bs *BrowserServer) handleSelect(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
407 | 	args := request.GetArguments()
408 | 	selector, ok := args["selector"].(string)
409 | 	if !ok {
410 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to select selector:%v", args["selector"])), nil
411 | 	}
412 | 	value, ok := args["value"].(string)
413 | 	if !ok {
414 | 		return mcp.NewToolResultError(fmt.Sprintf("failed to select value:%v", args["value"])), nil
415 | 	}
416 | 	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
417 | 	defer cancelFunc()
418 | 	err := chromedp.Run(runCtx, chromedp.SetValue(selector, value, chromedp.NodeVisible))
419 | 	if err != nil {
420 | 		return mcp.NewToolResultError(fmt.Errorf("failed to select value: %s", err.Error()).Error()), nil
421 | 	}
422 | 	return mcp.NewToolResultText(fmt.Sprintf("Selected value %s for element %s", value, selector)), nil
423 | }
424 | 
425 | // handleHover handles the hover action on a specified element.
426 | func (bs *BrowserServer) handleHover(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
427 | 	args := request.GetArguments()
428 | 	selector, ok := args["selector"].(string)
429 | 	if !ok {
430 | 		return mcp.NewToolResultError(fmt.Sprintf("selector must be a string:%v", selector)), nil
431 | 	}
432 | 	var res bool
433 | 	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
434 | 	defer cancelFunc()
435 | 	err := chromedp.Run(runCtx, chromedp.Evaluate(`document.querySelector('`+selector+`').dispatchEvent(new Event('mouseover'))`, &res))
436 | 	if err != nil {
437 | 		return mcp.NewToolResultError(fmt.Errorf("failed to hover over element: %s", err.Error()).Error()), nil
438 | 	}
439 | 	return mcp.NewToolResultText(fmt.Sprintf("Hovered over element %s, result:%t", selector, res)), nil
440 | }
441 | 
442 | func (bs *BrowserServer) handleEvaluate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
443 | 	args := request.GetArguments()
444 | 	script, ok := args["script"].(string)
445 | 	if !ok {
446 | 		return mcp.NewToolResultError("script must be a string"), nil
447 | 	}
448 | 	var result any
449 | 	runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
450 | 	defer cancelFunc()
451 | 	err := chromedp.Run(runCtx, chromedp.Evaluate(script, &result))
452 | 	if err != nil {
453 | 		return mcp.NewToolResultError(fmt.Errorf("failed to execute script: %s", err.Error()).Error()), nil
454 | 	}
455 | 	return mcp.NewToolResultText(fmt.Sprintf("Script executed successfully: %v", result)), nil
456 | }
457 | 
458 | func (bs *BrowserServer) Close() error {
459 | 	bs.Logger.Debug().Msg("Closing browser server")
460 | 	bs.cancelAlloc()
461 | 	bs.cancelChrome()
462 | 	// Cancel the context to stop the browser
463 | 	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
464 | 	defer cancel()
465 | 	return chromedp.Cancel(ctx)
466 | }
467 | 
468 | // Config returns the configuration of the service as a string.
469 | func (bs *BrowserServer) Config() string {
470 | 	cfg, err := json.Marshal(bs.config)
471 | 	if err != nil {
472 | 		bs.Logger.Err(err).Msg("failed to marshal config")
473 | 		return "{}"
474 | 	}
475 | 	return string(cfg)
476 | }
477 | 
478 | func (bs *BrowserServer) Name() comm.MoLingServerType {
479 | 	return BrowserServerName
480 | }
481 | 
482 | // LoadConfig loads the configuration from a JSON object.
483 | func (bs *BrowserServer) LoadConfig(jsonData map[string]any) error {
484 | 	err := utils.MergeJSONToStruct(bs.config, jsonData)
485 | 	if err != nil {
486 | 		return err
487 | 	}
488 | 	return bs.config.Check()
489 | }
490 | 
```

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

```go
   1 | // Copyright 2025 CFC4N <[email protected]>. All Rights Reserved.
   2 | //
   3 | // Licensed under the Apache License, Version 2.0 (the "License");
   4 | // you may not use this file except in compliance with the License.
   5 | // You may obtain a copy of the License at
   6 | //
   7 | //   http://www.apache.org/licenses/LICENSE-2.0
   8 | //
   9 | // Unless required by applicable law or agreed to in writing, software
  10 | // distributed under the License is distributed on an "AS IS" BASIS,
  11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 | // See the License for the specific language governing permissions and
  13 | // limitations under the License.
  14 | //
  15 | // Repository: https://github.com/gojue/moling
  16 | // Source: https://github.com/mark3labs/mcp-filesystem-server
  17 | 
  18 | // Package services provides the implementation of the FileSystemServer, which allows access to files and directories on the local file system.
  19 | package filesystem
  20 | 
  21 | import (
  22 | 	"context"
  23 | 	"encoding/base64"
  24 | 	"encoding/json"
  25 | 	"fmt"
  26 | 	"os"
  27 | 	"path/filepath"
  28 | 	"strings"
  29 | 	"time"
  30 | 
  31 | 	"github.com/mark3labs/mcp-go/mcp"
  32 | 	"github.com/rs/zerolog"
  33 | 
  34 | 	"github.com/gojue/moling/pkg/comm"
  35 | 	"github.com/gojue/moling/pkg/config"
  36 | 	"github.com/gojue/moling/pkg/services/abstract"
  37 | 	"github.com/gojue/moling/pkg/utils"
  38 | )
  39 | 
  40 | const (
  41 | 	// MaxInlineSize Maximum size for inline content (5MB)
  42 | 	MaxInlineSize = 1024 * 1024 * 5
  43 | 	// MaxBase64Size Maximum size for base64 encoding (1MB)
  44 | 	MaxBase64Size = 1024 * 1024 * 1
  45 | )
  46 | const (
  47 | 	FilesystemServerName comm.MoLingServerType = "FileSystem"
  48 | )
  49 | 
  50 | type FileInfo struct {
  51 | 	Size        int64     `json:"size"`
  52 | 	Created     time.Time `json:"created"`
  53 | 	Modified    time.Time `json:"modified"`
  54 | 	Accessed    time.Time `json:"accessed"`
  55 | 	IsDirectory bool      `json:"isDirectory"`
  56 | 	IsFile      bool      `json:"isFile"`
  57 | 	Permissions string    `json:"permissions"`
  58 | }
  59 | 
  60 | type FilesystemServer struct {
  61 | 	abstract.MLService
  62 | 	config *FileSystemConfig
  63 | }
  64 | 
  65 | func NewFilesystemServer(ctx context.Context) (abstract.Service, error) {
  66 | 	// Validate the config
  67 | 	var err error
  68 | 	globalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)
  69 | 	userDataDir := filepath.Join(globalConf.BasePath, "data")
  70 | 
  71 | 	fc := NewFileSystemConfig(userDataDir)
  72 | 
  73 | 	lger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)
  74 | 	if !ok {
  75 | 		return nil, fmt.Errorf("FilesystemServer: invalid logger type")
  76 | 	}
  77 | 
  78 | 	loggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
  79 | 		e.Str("Service", string(FilesystemServerName))
  80 | 	})
  81 | 
  82 | 	fs := &FilesystemServer{
  83 | 		MLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), globalConf),
  84 | 		config:    fc,
  85 | 	}
  86 | 
  87 | 	err = fs.InitResources()
  88 | 	if err != nil {
  89 | 		return nil, fmt.Errorf("failed to initialize filesystem server: %w", err)
  90 | 	}
  91 | 
  92 | 	return fs, nil
  93 | }
  94 | 
  95 | func (fs *FilesystemServer) Init() error {
  96 | 	// Register resource handlers
  97 | 	fs.AddResource(mcp.NewResource("file://", "File System",
  98 | 		mcp.WithResourceDescription("Access to files and directories on the local file system"),
  99 | 	), fs.handleReadResource)
 100 | 
 101 | 	pe := abstract.PromptEntry{
 102 | 		PromptVar: mcp.Prompt{
 103 | 			Name:        "filesystem_prompt",
 104 | 			Description: "Get the relevant functions and prompts of the FileSystem MCP Server.",
 105 | 		},
 106 | 		HandlerFunc: fs.handlePrompt,
 107 | 	}
 108 | 	fs.AddPrompt(pe)
 109 | 
 110 | 	// Register tool handlers
 111 | 	fs.AddTool(mcp.NewTool("read_file",
 112 | 		mcp.WithDescription("Read the complete contents of a file from the file system."),
 113 | 		mcp.WithString("path",
 114 | 			mcp.Description("Relative path to the file to read"),
 115 | 			mcp.Required(),
 116 | 		),
 117 | 	), fs.handleReadFile)
 118 | 
 119 | 	fs.AddTool(mcp.NewTool(
 120 | 		"write_file",
 121 | 		mcp.WithDescription("Create a new file or overwrite an existing file with new content."),
 122 | 		mcp.WithString("path",
 123 | 			mcp.Description("Relative Path where to write the file"),
 124 | 			mcp.Required(),
 125 | 		),
 126 | 		mcp.WithString("content",
 127 | 			mcp.Description("Content to write to the file"),
 128 | 			mcp.Required(),
 129 | 		),
 130 | 	), fs.handleWriteFile)
 131 | 
 132 | 	fs.AddTool(mcp.NewTool(
 133 | 		"list_directory",
 134 | 		mcp.WithDescription("Get a detailed listing of all files and directories in a specified path."),
 135 | 		mcp.WithString("path",
 136 | 			mcp.Description("Relative Path of the directory to list"),
 137 | 			mcp.Required(),
 138 | 		),
 139 | 	), fs.handleListDirectory)
 140 | 
 141 | 	fs.AddTool(mcp.NewTool(
 142 | 		"create_directory",
 143 | 		mcp.WithDescription("Create a new directory or ensure a directory exists."),
 144 | 		mcp.WithString("path",
 145 | 			mcp.Description("Relative Path of the directory to create"),
 146 | 			mcp.Required(),
 147 | 		),
 148 | 	), fs.handleCreateDirectory)
 149 | 
 150 | 	fs.AddTool(mcp.NewTool(
 151 | 		"move_file",
 152 | 		mcp.WithDescription("Move or rename files and directories."),
 153 | 		mcp.WithString("source",
 154 | 			mcp.Description("Relative Source path of the file or directory"),
 155 | 			mcp.Required(),
 156 | 		),
 157 | 		mcp.WithString("destination",
 158 | 			mcp.Description("Relative Destination path"),
 159 | 			mcp.Required(),
 160 | 		),
 161 | 	), fs.handleMoveFile)
 162 | 
 163 | 	fs.AddTool(mcp.NewTool(
 164 | 		"search_files",
 165 | 		mcp.WithDescription("Recursively search for files and directories matching a pattern."),
 166 | 		mcp.WithString("path",
 167 | 			mcp.Description("Relative Starting path for the search"),
 168 | 			mcp.Required(),
 169 | 		),
 170 | 		mcp.WithString("pattern",
 171 | 			mcp.Description("Relative Search pattern to match against file names"),
 172 | 			mcp.Required(),
 173 | 		),
 174 | 	), fs.handleSearchFiles)
 175 | 
 176 | 	fs.AddTool(mcp.NewTool(
 177 | 		"get_file_info",
 178 | 		mcp.WithDescription("Retrieve detailed metadata about a file or directory."),
 179 | 		mcp.WithString("path",
 180 | 			mcp.Description("Relative Path to the file or directory"),
 181 | 			mcp.Required(),
 182 | 		),
 183 | 	), fs.handleGetFileInfo)
 184 | 
 185 | 	fs.AddTool(mcp.NewTool(
 186 | 		"list_allowed_directories",
 187 | 		mcp.WithDescription("Returns the list of directories that this server is allowed to access."),
 188 | 	), fs.handleListAllowedDirectories)
 189 | 	return nil
 190 | }
 191 | 
 192 | // handlePrompt handles the prompt request for the FilesystemServer
 193 | func (fs *FilesystemServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 194 | 	return &mcp.GetPromptResult{
 195 | 		Description: "",
 196 | 		Messages: []mcp.PromptMessage{
 197 | 			{
 198 | 				Role: mcp.RoleUser,
 199 | 				Content: mcp.TextContent{
 200 | 					Type: "text",
 201 | 					Text: fs.config.prompt,
 202 | 				},
 203 | 			},
 204 | 		},
 205 | 	}, nil
 206 | }
 207 | 
 208 | // isPathInAllowedDirs checks if a path is within any of the allowed directories
 209 | func (fs *FilesystemServer) isPathInAllowedDirs(path string) bool {
 210 | 	// Ensure path is absolute and clean
 211 | 	absPath, err := filepath.Abs(path)
 212 | 	if err != nil {
 213 | 		return false
 214 | 	}
 215 | 
 216 | 	// Add trailing separator to ensure we're checking a directory or a file within a directory
 217 | 	// and not a prefix match (e.g., /tmp/foo should not match /tmp/foobar)
 218 | 	if !strings.HasSuffix(absPath, string(filepath.Separator)) {
 219 | 		// If it'fss a file, we need to check its directory
 220 | 		if info, err := os.Stat(absPath); err == nil && !info.IsDir() {
 221 | 			absPath = filepath.Dir(absPath) + string(filepath.Separator)
 222 | 		} else {
 223 | 			absPath = absPath + string(filepath.Separator)
 224 | 		}
 225 | 	}
 226 | 
 227 | 	// Check if the path is within any of the allowed directories
 228 | 	for _, dir := range fs.config.allowedDirs {
 229 | 		if strings.HasPrefix(absPath, dir) {
 230 | 			return true
 231 | 		}
 232 | 	}
 233 | 	return false
 234 | }
 235 | 
 236 | func (fs *FilesystemServer) validatePath(requestedPath string) (string, error) {
 237 | 	// Always convert to absolute path first
 238 | 	var hasPrefix bool
 239 | 	var firstDir string
 240 | 	for _, dir := range fs.config.allowedDirs {
 241 | 		if firstDir == "" {
 242 | 			firstDir = dir
 243 | 		}
 244 | 		if strings.HasPrefix(requestedPath, dir) {
 245 | 			hasPrefix = true
 246 | 			break
 247 | 		}
 248 | 	}
 249 | 	if !hasPrefix {
 250 | 		requestedPath = filepath.Join(firstDir, requestedPath)
 251 | 	}
 252 | 	abs, err := filepath.Abs(requestedPath)
 253 | 	if err != nil {
 254 | 		return "", fmt.Errorf("invalid path: %w", err)
 255 | 	}
 256 | 
 257 | 	// Check if path is within allowed directories
 258 | 	if !fs.isPathInAllowedDirs(abs) {
 259 | 		return "", fmt.Errorf("access denied - path outside allowed directories: %s", abs)
 260 | 	}
 261 | 
 262 | 	// Handle symlinks
 263 | 	realPath, err := filepath.EvalSymlinks(abs)
 264 | 	if err != nil {
 265 | 		if !os.IsNotExist(err) {
 266 | 			return "", err
 267 | 		}
 268 | 		// For new files, check parent directory
 269 | 		parent := filepath.Dir(abs)
 270 | 		realParent, err := filepath.EvalSymlinks(parent)
 271 | 		if err != nil {
 272 | 			return "", fmt.Errorf("parent directory does not exist: %s", parent)
 273 | 		}
 274 | 
 275 | 		if !fs.isPathInAllowedDirs(realParent) {
 276 | 			return "", fmt.Errorf(
 277 | 				"access denied - parent directory outside allowed directories",
 278 | 			)
 279 | 		}
 280 | 		return abs, nil
 281 | 	}
 282 | 
 283 | 	// Check if the real path (after resolving symlinks) is still within allowed directories
 284 | 	if !fs.isPathInAllowedDirs(realPath) {
 285 | 		return "", fmt.Errorf(
 286 | 			"access denied - symlink target outside allowed directories",
 287 | 		)
 288 | 	}
 289 | 
 290 | 	return realPath, nil
 291 | }
 292 | 
 293 | func (fs *FilesystemServer) getFileStats(path string) (FileInfo, error) {
 294 | 	info, err := os.Stat(path)
 295 | 	if err != nil {
 296 | 		return FileInfo{}, err
 297 | 	}
 298 | 
 299 | 	return FileInfo{
 300 | 		Size:        info.Size(),
 301 | 		Created:     info.ModTime(), // Note: ModTime used as birth time isn't always available
 302 | 		Modified:    info.ModTime(),
 303 | 		Accessed:    info.ModTime(), // Note: Access time isn't always available
 304 | 		IsDirectory: info.IsDir(),
 305 | 		IsFile:      !info.IsDir(),
 306 | 		Permissions: fmt.Sprintf("%o", info.Mode().Perm()),
 307 | 	}, nil
 308 | }
 309 | 
 310 | func (fs *FilesystemServer) searchFiles(rootPath, pattern string) ([]string, error) {
 311 | 	var results []string
 312 | 	pattern = strings.ToLower(pattern)
 313 | 
 314 | 	err := filepath.Walk(
 315 | 		rootPath,
 316 | 		func(path string, info os.FileInfo, err error) error {
 317 | 			if err != nil {
 318 | 				return nil // Skip errors and continue
 319 | 			}
 320 | 
 321 | 			// Try to validate path
 322 | 			if _, err := fs.validatePath(path); err != nil {
 323 | 				return nil // Skip invalid paths
 324 | 			}
 325 | 
 326 | 			if strings.Contains(strings.ToLower(info.Name()), pattern) {
 327 | 				results = append(results, path)
 328 | 			}
 329 | 			return nil
 330 | 		},
 331 | 	)
 332 | 	if err != nil {
 333 | 		return nil, err
 334 | 	}
 335 | 	return results, nil
 336 | }
 337 | 
 338 | // Resource handler
 339 | func (fs *FilesystemServer) handleReadResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
 340 | 	uri := request.Params.URI
 341 | 	fs.Logger.Debug().Str("uri", uri).Msg("handleReadResource")
 342 | 
 343 | 	// Check if it'fss a file:// URI
 344 | 	if !strings.HasPrefix(uri, "file://") {
 345 | 		return nil, fmt.Errorf("unsupported URI scheme: %s", uri)
 346 | 	}
 347 | 
 348 | 	// Extract the path from the URI
 349 | 	path := strings.TrimPrefix(uri, "file://")
 350 | 
 351 | 	// Validate the path
 352 | 	validPath, err := fs.validatePath(path)
 353 | 	if err != nil {
 354 | 		return nil, err
 355 | 	}
 356 | 
 357 | 	// Get file info
 358 | 	fileInfo, err := os.Stat(validPath)
 359 | 	if err != nil {
 360 | 		return nil, err
 361 | 	}
 362 | 
 363 | 	// If it'fss a directory, return a listing
 364 | 	if fileInfo.IsDir() {
 365 | 		entries, err := os.ReadDir(validPath)
 366 | 		if err != nil {
 367 | 			return nil, err
 368 | 		}
 369 | 
 370 | 		var result strings.Builder
 371 | 		result.WriteString(fmt.Sprintf("Directory listing for: %s\n\n", validPath))
 372 | 
 373 | 		for _, entry := range entries {
 374 | 			entryPath := filepath.Join(validPath, entry.Name())
 375 | 			entryURI := utils.PathToResourceURI(entryPath)
 376 | 
 377 | 			if entry.IsDir() {
 378 | 				result.WriteString(fmt.Sprintf("[DIR]  %s (%s)\n", entry.Name(), entryURI))
 379 | 			} else {
 380 | 				info, err := entry.Info()
 381 | 				if err == nil {
 382 | 					result.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n",
 383 | 						entry.Name(), entryURI, info.Size()))
 384 | 				} else {
 385 | 					result.WriteString(fmt.Sprintf("[FILE] %s (%s)\n", entry.Name(), entryURI))
 386 | 				}
 387 | 			}
 388 | 		}
 389 | 
 390 | 		return []mcp.ResourceContents{
 391 | 			mcp.TextResourceContents{
 392 | 				URI:      uri,
 393 | 				MIMEType: "text/plain",
 394 | 				Text:     result.String(),
 395 | 			},
 396 | 		}, nil
 397 | 	}
 398 | 
 399 | 	// It'fss a file, determine how to handle it
 400 | 	mimeType := utils.DetectMimeType(validPath)
 401 | 
 402 | 	// Check file size
 403 | 	if fileInfo.Size() > MaxInlineSize {
 404 | 		// File is too large to inline, return a reference instead
 405 | 		return []mcp.ResourceContents{
 406 | 			mcp.TextResourceContents{
 407 | 				URI:      uri,
 408 | 				MIMEType: "text/plain",
 409 | 				Text:     fmt.Sprintf("File is too large to display inline (%d bytes). Use the read_file tool to access specific portions.", fileInfo.Size()),
 410 | 			},
 411 | 		}, nil
 412 | 	}
 413 | 
 414 | 	// Read the file content
 415 | 	content, err := os.ReadFile(validPath)
 416 | 	if err != nil {
 417 | 		return nil, err
 418 | 	}
 419 | 
 420 | 	// Handle based on content type
 421 | 	if utils.IsTextFile(mimeType) {
 422 | 		// It'fss a text file, return as text
 423 | 		return []mcp.ResourceContents{
 424 | 			mcp.TextResourceContents{
 425 | 				URI:      uri,
 426 | 				MIMEType: mimeType,
 427 | 				Text:     string(content),
 428 | 			},
 429 | 		}, nil
 430 | 	} else {
 431 | 		// It'fss a binary file
 432 | 		if fileInfo.Size() <= MaxBase64Size {
 433 | 			// Small enough for base64 encoding
 434 | 			return []mcp.ResourceContents{
 435 | 				mcp.BlobResourceContents{
 436 | 					URI:      uri,
 437 | 					MIMEType: mimeType,
 438 | 					Blob:     base64.StdEncoding.EncodeToString(content),
 439 | 				},
 440 | 			}, nil
 441 | 		} else {
 442 | 			// Too large for base64, return a reference
 443 | 			return []mcp.ResourceContents{
 444 | 				mcp.TextResourceContents{
 445 | 					URI:      uri,
 446 | 					MIMEType: "text/plain",
 447 | 					Text:     fmt.Sprintf("Binary file (%s, %d bytes). Use the read_file tool to access specific portions.", mimeType, fileInfo.Size()),
 448 | 				},
 449 | 			}, nil
 450 | 		}
 451 | 	}
 452 | }
 453 | 
 454 | // Tool handlers
 455 | 
 456 | func (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 457 | 	args := request.GetArguments()
 458 | 	path, ok := args["path"].(string)
 459 | 	if !ok {
 460 | 		return mcp.NewToolResultError("Path must be a string"), nil
 461 | 	}
 462 | 
 463 | 	// 判断 前缀是不是已经包含了
 464 | 	//path = filepath.Join(fss.config.CachePath, path)
 465 | 	validPath, err := fs.validatePath(path)
 466 | 	if err != nil {
 467 | 		return mcp.NewToolResultError(fmt.Sprintf("validate Path Error: %v", err)), nil
 468 | 	}
 469 | 
 470 | 	// Check if it'fss a directory
 471 | 	info, err := os.Stat(validPath)
 472 | 	if err != nil {
 473 | 		return mcp.NewToolResultError(fmt.Sprintf("check directory error: %v", err)), nil
 474 | 	}
 475 | 
 476 | 	if info.IsDir() {
 477 | 		// For directories, return a resource reference instead
 478 | 		resourceURI := utils.PathToResourceURI(validPath)
 479 | 		return &mcp.CallToolResult{
 480 | 			Content: []mcp.Content{
 481 | 				mcp.TextContent{
 482 | 					Type: "text",
 483 | 					Text: fmt.Sprintf("This is a directory. Use the resource URI to browse its contents: %s", resourceURI),
 484 | 				},
 485 | 				mcp.EmbeddedResource{
 486 | 					Type: "resource",
 487 | 					Resource: mcp.TextResourceContents{
 488 | 						URI:      resourceURI,
 489 | 						MIMEType: "text/plain",
 490 | 						Text:     fmt.Sprintf("Directory: %s", validPath),
 491 | 					},
 492 | 				},
 493 | 			},
 494 | 		}, nil
 495 | 	}
 496 | 
 497 | 	// Determine MIME type
 498 | 	mimeType := utils.DetectMimeType(validPath)
 499 | 
 500 | 	// Check file size
 501 | 	if info.Size() > MaxInlineSize {
 502 | 		// File is too large to inline, return a resource reference
 503 | 		resourceURI := utils.PathToResourceURI(validPath)
 504 | 		return &mcp.CallToolResult{
 505 | 			Content: []mcp.Content{
 506 | 				mcp.TextContent{
 507 | 					Type: "text",
 508 | 					Text: fmt.Sprintf("File is too large to display inline (%d bytes). Access it via resource URI: %s", info.Size(), resourceURI),
 509 | 				},
 510 | 				mcp.EmbeddedResource{
 511 | 					Type: "resource",
 512 | 					Resource: mcp.TextResourceContents{
 513 | 						URI:      resourceURI,
 514 | 						MIMEType: "text/plain",
 515 | 						Text:     fmt.Sprintf("Large file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
 516 | 					},
 517 | 				},
 518 | 			},
 519 | 		}, nil
 520 | 	}
 521 | 
 522 | 	// Read file content
 523 | 	content, err := os.ReadFile(validPath)
 524 | 	if err != nil {
 525 | 		return mcp.NewToolResultError(fmt.Sprintf("Error reading file: %v", err)), nil
 526 | 	}
 527 | 
 528 | 	// Handle based on content type
 529 | 	if utils.IsTextFile(mimeType) {
 530 | 		// It'fss a text file, return as text
 531 | 		return mcp.NewToolResultText(string(content)), nil
 532 | 	} else if utils.IsImageFile(mimeType) {
 533 | 		// It'fss an image file, return as image content
 534 | 		if info.Size() <= MaxBase64Size {
 535 | 			return &mcp.CallToolResult{
 536 | 				Content: []mcp.Content{
 537 | 					mcp.TextContent{
 538 | 						Type: "text",
 539 | 						Text: fmt.Sprintf("Image file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
 540 | 					},
 541 | 					mcp.ImageContent{
 542 | 						Type:     "image",
 543 | 						Data:     base64.StdEncoding.EncodeToString(content),
 544 | 						MIMEType: mimeType,
 545 | 					},
 546 | 				},
 547 | 			}, nil
 548 | 		} else {
 549 | 			// Too large for base64, return a reference
 550 | 			resourceURI := utils.PathToResourceURI(validPath)
 551 | 			return &mcp.CallToolResult{
 552 | 				Content: []mcp.Content{
 553 | 					mcp.TextContent{
 554 | 						Type: "text",
 555 | 						Text: fmt.Sprintf("Image file is too large to display inline (%d bytes). Access it via resource URI: %s", info.Size(), resourceURI),
 556 | 					},
 557 | 					mcp.EmbeddedResource{
 558 | 						Type: "resource",
 559 | 						Resource: mcp.TextResourceContents{
 560 | 							URI:      resourceURI,
 561 | 							MIMEType: "text/plain",
 562 | 							Text:     fmt.Sprintf("Large image: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
 563 | 						},
 564 | 					},
 565 | 				},
 566 | 			}, nil
 567 | 		}
 568 | 	} else {
 569 | 		// It'fss another type of binary file
 570 | 		resourceURI := utils.PathToResourceURI(validPath)
 571 | 
 572 | 		if info.Size() <= MaxBase64Size {
 573 | 			// Small enough for base64 encoding
 574 | 			return &mcp.CallToolResult{
 575 | 				Content: []mcp.Content{
 576 | 					mcp.TextContent{
 577 | 						Type: "text",
 578 | 						Text: fmt.Sprintf("Binary file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
 579 | 					},
 580 | 					mcp.EmbeddedResource{
 581 | 						Type: "resource",
 582 | 						Resource: mcp.BlobResourceContents{
 583 | 							URI:      resourceURI,
 584 | 							MIMEType: mimeType,
 585 | 							Blob:     base64.StdEncoding.EncodeToString(content),
 586 | 						},
 587 | 					},
 588 | 				},
 589 | 			}, nil
 590 | 		} else {
 591 | 			// Too large for base64, return a reference
 592 | 			return &mcp.CallToolResult{
 593 | 				Content: []mcp.Content{
 594 | 					mcp.TextContent{
 595 | 						Type: "text",
 596 | 						Text: fmt.Sprintf("Binary file: %s (%s, %d bytes). Access it via resource URI: %s", validPath, mimeType, info.Size(), resourceURI),
 597 | 					},
 598 | 					mcp.EmbeddedResource{
 599 | 						Type: "resource",
 600 | 						Resource: mcp.TextResourceContents{
 601 | 							URI:      resourceURI,
 602 | 							MIMEType: "text/plain",
 603 | 							Text:     fmt.Sprintf("Binary file: %s (%s, %d bytes)", validPath, mimeType, info.Size()),
 604 | 						},
 605 | 					},
 606 | 				},
 607 | 			}, nil
 608 | 		}
 609 | 	}
 610 | }
 611 | 
 612 | func (fs *FilesystemServer) handleWriteFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 613 | 	args := request.GetArguments()
 614 | 	path, ok := args["path"].(string)
 615 | 	if !ok {
 616 | 		return mcp.NewToolResultError("Path must be a string"), nil
 617 | 	}
 618 | 	content, ok := args["content"].(string)
 619 | 	if !ok {
 620 | 		return mcp.NewToolResultError("Content must be a string"), nil
 621 | 	}
 622 | 
 623 | 	//path = filepath.Join(fss.config.CachePath, path)
 624 | 
 625 | 	validPath, err := fs.validatePath(path)
 626 | 	if err != nil {
 627 | 		return &mcp.CallToolResult{
 628 | 			Content: []mcp.Content{
 629 | 				mcp.TextContent{
 630 | 					Type: "text",
 631 | 					Text: fmt.Sprintf("Error: %v", err),
 632 | 				},
 633 | 			},
 634 | 			IsError: true,
 635 | 		}, nil
 636 | 	}
 637 | 
 638 | 	// Check if it'fss a directory
 639 | 	if info, err := os.Stat(validPath); err == nil && info.IsDir() {
 640 | 		return mcp.NewToolResultError(fmt.Sprintf("Error: Cannot write to a directory:%s", validPath)), nil
 641 | 	}
 642 | 
 643 | 	// Create parent directories if they don't exist
 644 | 	parentDir := filepath.Dir(validPath)
 645 | 	if err := os.MkdirAll(parentDir, 0755); err != nil {
 646 | 		return mcp.NewToolResultError(fmt.Sprintf("Error creating parent directories: %v", err)), nil
 647 | 	}
 648 | 
 649 | 	if err := os.WriteFile(validPath, []byte(content), 0644); err != nil {
 650 | 		return mcp.NewToolResultError(fmt.Sprintf("Error writing file: %v", err)), nil
 651 | 	}
 652 | 
 653 | 	// Get file info for the response
 654 | 	info, err := os.Stat(validPath)
 655 | 	if err != nil {
 656 | 		// File was written but we couldn't get info
 657 | 		return mcp.NewToolResultText(fmt.Sprintf("Successfully wrote to %s", path)), nil
 658 | 	}
 659 | 
 660 | 	resourceURI := utils.PathToResourceURI(validPath)
 661 | 	return &mcp.CallToolResult{
 662 | 		Content: []mcp.Content{
 663 | 			mcp.TextContent{
 664 | 				Type: "text",
 665 | 				Text: fmt.Sprintf("Successfully wrote %d bytes to %s", info.Size(), path),
 666 | 			},
 667 | 			mcp.EmbeddedResource{
 668 | 				Type: "resource",
 669 | 				Resource: mcp.TextResourceContents{
 670 | 					URI:      resourceURI,
 671 | 					MIMEType: "text/plain",
 672 | 					Text:     fmt.Sprintf("File: %s (%d bytes)", validPath, info.Size()),
 673 | 				},
 674 | 			},
 675 | 		},
 676 | 	}, nil
 677 | }
 678 | 
 679 | func (fs *FilesystemServer) handleListDirectory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 680 | 	args := request.GetArguments()
 681 | 	path, ok := args["path"].(string)
 682 | 	if !ok {
 683 | 		return mcp.NewToolResultError("Path must be a string"), nil
 684 | 	}
 685 | 
 686 | 	validPath, err := fs.validatePath(path)
 687 | 	if err != nil {
 688 | 		return mcp.NewToolResultError(fmt.Sprintf("validate path error: %v, path:%s", err, validPath)), nil
 689 | 	}
 690 | 
 691 | 	// Check if it'fss a directory
 692 | 	info, err := os.Stat(validPath)
 693 | 	if err != nil {
 694 | 		return mcp.NewToolResultError(fmt.Sprintf("Check directory %s Error: %v", validPath, err)), nil
 695 | 	}
 696 | 
 697 | 	if !info.IsDir() {
 698 | 		return mcp.NewToolResultError(fmt.Sprintf("Error: Path is not a directory:%s", validPath)), nil
 699 | 	}
 700 | 
 701 | 	entries, err := os.ReadDir(validPath)
 702 | 	if err != nil {
 703 | 		return mcp.NewToolResultError(fmt.Sprintf("Error reading directory: %v", err)), nil
 704 | 	}
 705 | 
 706 | 	var result strings.Builder
 707 | 	result.WriteString(fmt.Sprintf("Directory listing for: %s\n\n", validPath))
 708 | 
 709 | 	for _, entry := range entries {
 710 | 		entryPath := filepath.Join(validPath, entry.Name())
 711 | 		resourceURI := utils.PathToResourceURI(entryPath)
 712 | 
 713 | 		if entry.IsDir() {
 714 | 			result.WriteString(fmt.Sprintf("[DIR]  %s (%s)\n", entry.Name(), resourceURI))
 715 | 		} else {
 716 | 			info, err := entry.Info()
 717 | 			if err == nil {
 718 | 				result.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n",
 719 | 					entry.Name(), resourceURI, info.Size()))
 720 | 			} else {
 721 | 				result.WriteString(fmt.Sprintf("[FILE] %s (%s)\n", entry.Name(), resourceURI))
 722 | 			}
 723 | 		}
 724 | 	}
 725 | 
 726 | 	// Return both text content and embedded resource
 727 | 	resourceURI := utils.PathToResourceURI(validPath)
 728 | 	return &mcp.CallToolResult{
 729 | 		Content: []mcp.Content{
 730 | 			mcp.TextContent{
 731 | 				Type: "text",
 732 | 				Text: result.String(),
 733 | 			},
 734 | 			mcp.EmbeddedResource{
 735 | 				Type: "resource",
 736 | 				Resource: mcp.TextResourceContents{
 737 | 					URI:      resourceURI,
 738 | 					MIMEType: "text/plain",
 739 | 					Text:     fmt.Sprintf("Directory: %s", validPath),
 740 | 				},
 741 | 			},
 742 | 		},
 743 | 	}, nil
 744 | }
 745 | 
 746 | func (fs *FilesystemServer) handleCreateDirectory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 747 | 	args := request.GetArguments()
 748 | 	path, ok := args["path"].(string)
 749 | 	if !ok {
 750 | 		return mcp.NewToolResultError("path must be a string"), nil
 751 | 	}
 752 | 
 753 | 	validPath, err := fs.validatePath(path)
 754 | 	if err != nil {
 755 | 		return mcp.NewToolResultError(fmt.Sprintf("Error: %v", err)), nil
 756 | 	}
 757 | 
 758 | 	// Check if path already exists
 759 | 	if info, err := os.Stat(validPath); err == nil {
 760 | 		if info.IsDir() {
 761 | 			resourceURI := utils.PathToResourceURI(validPath)
 762 | 			return &mcp.CallToolResult{
 763 | 				Content: []mcp.Content{
 764 | 					mcp.TextContent{
 765 | 						Type: "text",
 766 | 						Text: fmt.Sprintf("Directory already exists: %s", path),
 767 | 					},
 768 | 					mcp.EmbeddedResource{
 769 | 						Type: "resource",
 770 | 						Resource: mcp.TextResourceContents{
 771 | 							URI:      resourceURI,
 772 | 							MIMEType: "text/plain",
 773 | 							Text:     fmt.Sprintf("Directory: %s", validPath),
 774 | 						},
 775 | 					},
 776 | 				},
 777 | 			}, nil
 778 | 		}
 779 | 		return mcp.NewToolResultError(fmt.Sprintf("Error: Path exists but is not a directory: %s", path)), nil
 780 | 	}
 781 | 
 782 | 	if err := os.MkdirAll(validPath, 0755); err != nil {
 783 | 		return mcp.NewToolResultError(fmt.Sprintf("Error creating directory: %v", err)), nil
 784 | 	}
 785 | 
 786 | 	resourceURI := utils.PathToResourceURI(validPath)
 787 | 	return &mcp.CallToolResult{
 788 | 		Content: []mcp.Content{
 789 | 			mcp.TextContent{
 790 | 				Type: "text",
 791 | 				Text: fmt.Sprintf("Successfully created directory %s", path),
 792 | 			},
 793 | 			mcp.EmbeddedResource{
 794 | 				Type: "resource",
 795 | 				Resource: mcp.TextResourceContents{
 796 | 					URI:      resourceURI,
 797 | 					MIMEType: "text/plain",
 798 | 					Text:     fmt.Sprintf("Directory: %s", validPath),
 799 | 				},
 800 | 			},
 801 | 		},
 802 | 	}, nil
 803 | }
 804 | 
 805 | func (fs *FilesystemServer) handleMoveFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 806 | 	args := request.GetArguments()
 807 | 	source, ok := args["source"].(string)
 808 | 	if !ok {
 809 | 		return mcp.NewToolResultError("source must be a string"), nil
 810 | 	}
 811 | 	destination, ok := args["destination"].(string)
 812 | 	if !ok {
 813 | 		return mcp.NewToolResultError("destination must be a string"), nil
 814 | 	}
 815 | 
 816 | 	validSource, err := fs.validatePath(source)
 817 | 	if err != nil {
 818 | 		return mcp.NewToolResultError(fmt.Sprintf("Error with source path: %v", err)), nil
 819 | 	}
 820 | 
 821 | 	// Check if source exists
 822 | 	if _, err := os.Stat(validSource); os.IsNotExist(err) {
 823 | 		return mcp.NewToolResultError(fmt.Sprintf("Error: Source does not exist: %s", source)), nil
 824 | 	}
 825 | 
 826 | 	validDest, err := fs.validatePath(destination)
 827 | 	if err != nil {
 828 | 		return mcp.NewToolResultError(fmt.Sprintf("Error with destination path: %v", err)), nil
 829 | 	}
 830 | 
 831 | 	// Create parent directory for destination if it doesn't exist
 832 | 	destDir := filepath.Dir(validDest)
 833 | 	if err := os.MkdirAll(destDir, 0755); err != nil {
 834 | 		return mcp.NewToolResultError(fmt.Sprintf("Error creating destination directory: %v", err)), nil
 835 | 	}
 836 | 
 837 | 	if err := os.Rename(validSource, validDest); err != nil {
 838 | 		return mcp.NewToolResultError(fmt.Sprintf("Error moving file: %v", err)), nil
 839 | 	}
 840 | 
 841 | 	resourceURI := utils.PathToResourceURI(validDest)
 842 | 	return &mcp.CallToolResult{
 843 | 		Content: []mcp.Content{
 844 | 			mcp.TextContent{
 845 | 				Type: "text",
 846 | 				Text: fmt.Sprintf(
 847 | 					"Successfully moved %s to %s",
 848 | 					source,
 849 | 					destination,
 850 | 				),
 851 | 			},
 852 | 			mcp.EmbeddedResource{
 853 | 				Type: "resource",
 854 | 				Resource: mcp.TextResourceContents{
 855 | 					URI:      resourceURI,
 856 | 					MIMEType: "text/plain",
 857 | 					Text:     fmt.Sprintf("Moved file: %s", validDest),
 858 | 				},
 859 | 			},
 860 | 		},
 861 | 	}, nil
 862 | }
 863 | 
 864 | func (fs *FilesystemServer) handleSearchFiles(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 865 | 	args := request.GetArguments()
 866 | 	path, ok := args["path"].(string)
 867 | 	if !ok {
 868 | 		return mcp.NewToolResultError("path must be a string"), nil
 869 | 	}
 870 | 	pattern, ok := args["pattern"].(string)
 871 | 	if !ok {
 872 | 		return mcp.NewToolResultError("pattern must be a string"), nil
 873 | 	}
 874 | 
 875 | 	validPath, err := fs.validatePath(path)
 876 | 	if err != nil {
 877 | 		return mcp.NewToolResultError(fmt.Sprintf("Error: %v", err)), nil
 878 | 	}
 879 | 
 880 | 	// Check if it'fss a directory
 881 | 	info, err := os.Stat(validPath)
 882 | 	if err != nil {
 883 | 		return mcp.NewToolResultError(fmt.Sprintf("Error: %v", err)), nil
 884 | 	}
 885 | 
 886 | 	if !info.IsDir() {
 887 | 		return mcp.NewToolResultError("Error: Search path must be a directory"), nil
 888 | 	}
 889 | 
 890 | 	results, err := fs.searchFiles(validPath, pattern)
 891 | 	if err != nil {
 892 | 		return mcp.NewToolResultError(fmt.Sprintf("Error searching files: %v", err)), nil
 893 | 	}
 894 | 
 895 | 	if len(results) == 0 {
 896 | 		return mcp.NewToolResultText(fmt.Sprintf("No files found matching pattern '%s' in %s", pattern, path)), nil
 897 | 	}
 898 | 
 899 | 	// Format results with resource URIs
 900 | 	var formattedResults strings.Builder
 901 | 	formattedResults.WriteString(fmt.Sprintf("Found %d results:\n\n", len(results)))
 902 | 
 903 | 	for _, result := range results {
 904 | 		resourceURI := utils.PathToResourceURI(result)
 905 | 		info, err := os.Stat(result)
 906 | 		if err == nil {
 907 | 			if info.IsDir() {
 908 | 				formattedResults.WriteString(fmt.Sprintf("[DIR]  %s (%s)\n", result, resourceURI))
 909 | 			} else {
 910 | 				formattedResults.WriteString(fmt.Sprintf("[FILE] %s (%s) - %d bytes\n",
 911 | 					result, resourceURI, info.Size()))
 912 | 			}
 913 | 		} else {
 914 | 			formattedResults.WriteString(fmt.Sprintf("%s (%s)\n", result, resourceURI))
 915 | 		}
 916 | 	}
 917 | 
 918 | 	return mcp.NewToolResultText(formattedResults.String()), nil
 919 | }
 920 | 
 921 | func (fs *FilesystemServer) handleGetFileInfo(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 922 | 	args := request.GetArguments()
 923 | 	path, ok := args["path"].(string)
 924 | 	if !ok {
 925 | 		return mcp.NewToolResultError(fmt.Errorf("path %v must be a string", args["path"]).Error()), nil
 926 | 	}
 927 | 
 928 | 	validPath, err := fs.validatePath(path)
 929 | 	if err != nil {
 930 | 		return &mcp.CallToolResult{
 931 | 			Content: []mcp.Content{
 932 | 				mcp.TextContent{
 933 | 					Type: "text",
 934 | 					Text: fmt.Sprintf("Error: %v", err),
 935 | 				},
 936 | 			},
 937 | 			IsError: true,
 938 | 		}, nil
 939 | 	}
 940 | 
 941 | 	info, err := fs.getFileStats(validPath)
 942 | 	if err != nil {
 943 | 		return mcp.NewToolResultError(fmt.Sprintf("Error getting file info: %v", err)), nil
 944 | 	}
 945 | 
 946 | 	// Get MIME type for files
 947 | 	mimeType := "directory"
 948 | 	if info.IsFile {
 949 | 		mimeType = utils.DetectMimeType(validPath)
 950 | 	}
 951 | 
 952 | 	resourceURI := utils.PathToResourceURI(validPath)
 953 | 
 954 | 	// Determine file type text
 955 | 	var fileTypeText string
 956 | 	if info.IsDirectory {
 957 | 		fileTypeText = "Directory"
 958 | 	} else {
 959 | 		fileTypeText = "File"
 960 | 	}
 961 | 
 962 | 	return &mcp.CallToolResult{
 963 | 		Content: []mcp.Content{
 964 | 			mcp.TextContent{
 965 | 				Type: "text",
 966 | 				Text: fmt.Sprintf(
 967 | 					"File information for: %s\n\nSize: %d bytes\nCreated: %s\nModified: %s\nAccessed: %s\nIsDirectory: %v\nIsFile: %v\nPermissions: %s\nMIME Type: %s\nResource URI: %s",
 968 | 					validPath,
 969 | 					info.Size,
 970 | 					info.Created.Format(time.RFC3339),
 971 | 					info.Modified.Format(time.RFC3339),
 972 | 					info.Accessed.Format(time.RFC3339),
 973 | 					info.IsDirectory,
 974 | 					info.IsFile,
 975 | 					info.Permissions,
 976 | 					mimeType,
 977 | 					resourceURI,
 978 | 				),
 979 | 			},
 980 | 			mcp.EmbeddedResource{
 981 | 				Type: "resource",
 982 | 				Resource: mcp.TextResourceContents{
 983 | 					URI:      resourceURI,
 984 | 					MIMEType: "text/plain",
 985 | 					Text: fmt.Sprintf("%s: %s (%s, %d bytes)",
 986 | 						fileTypeText,
 987 | 						validPath,
 988 | 						mimeType,
 989 | 						info.Size),
 990 | 				},
 991 | 			},
 992 | 		},
 993 | 	}, nil
 994 | }
 995 | 
 996 | func (fs *FilesystemServer) handleListAllowedDirectories(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 997 | 	// Remove the trailing separator for display purposes
 998 | 	displayDirs := make([]string, len(fs.config.allowedDirs))
 999 | 	for i, dir := range fs.config.allowedDirs {
1000 | 		displayDirs[i] = strings.TrimSuffix(dir, string(filepath.Separator))
1001 | 	}
1002 | 
1003 | 	var result strings.Builder
1004 | 	result.WriteString("Allowed directories:")
1005 | 
1006 | 	for _, dir := range displayDirs {
1007 | 		resourceURI := utils.PathToResourceURI(dir)
1008 | 		result.WriteString(fmt.Sprintf("%s (%s)\n", dir, resourceURI))
1009 | 	}
1010 | 
1011 | 	return mcp.NewToolResultText(result.String()), nil
1012 | }
1013 | 
1014 | // Config returns the configuration of the service as a string.
1015 | func (fs *FilesystemServer) Config() string {
1016 | 	fs.config.AllowedDir = strings.Join(fs.config.allowedDirs, ",")
1017 | 	cfg, err := json.Marshal(fs.config)
1018 | 	if err != nil {
1019 | 		fs.Logger.Err(err).Msg("failed to marshal config")
1020 | 		return "{}"
1021 | 	}
1022 | 	return string(cfg)
1023 | }
1024 | 
1025 | func (fs *FilesystemServer) Name() comm.MoLingServerType {
1026 | 	return FilesystemServerName
1027 | }
1028 | 
1029 | func (fs *FilesystemServer) Close() error {
1030 | 	// Cancel the context to stop the browser
1031 | 	fs.Logger.Debug().Msg("closing FilesystemServer")
1032 | 	return nil
1033 | }
1034 | 
1035 | // LoadConfig loads the configuration from a JSON object.
1036 | func (fs *FilesystemServer) LoadConfig(jsonData map[string]any) error {
1037 | 	err := utils.MergeJSONToStruct(fs.config, jsonData)
1038 | 	if err != nil {
1039 | 		return err
1040 | 	}
1041 | 	fs.config.allowedDirs = strings.Split(fs.config.AllowedDir, ",")
1042 | 	return fs.config.Check()
1043 | }
1044 | 
```
Page 2/2FirstPrevNextLast