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