mirror of
https://github.com/chaitin/SafeLine.git
synced 2025-11-25 19:37:42 +08:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d871e638b | ||
|
|
ad2aeb4cf9 | ||
|
|
8247d11dda | ||
|
|
63588b8f5e | ||
|
|
bf8d64c6b2 | ||
|
|
0b8b253efc | ||
|
|
24e2bac1bd | ||
|
|
8113dbb171 | ||
|
|
66c4e60255 | ||
|
|
eeae48affb | ||
|
|
75a5346e6f | ||
|
|
b6f694a376 | ||
|
|
9cab30cef1 | ||
|
|
8fd59844ea | ||
|
|
681e7f95f2 | ||
|
|
84c2db9ee3 | ||
|
|
c845231a1d | ||
|
|
91a26b8f93 | ||
|
|
24770cf5b4 | ||
|
|
ff4d6616fb | ||
|
|
a13d269c28 | ||
|
|
19db896a9a | ||
|
|
01c38c26d0 | ||
|
|
14fb45f648 | ||
|
|
f371d588a4 | ||
|
|
1317b3ed5a | ||
|
|
7fdf63310d | ||
|
|
1578ea6027 | ||
|
|
ec3dc96765 | ||
|
|
48e828cc03 | ||
|
|
78e71aae23 | ||
|
|
857e0c46ef | ||
|
|
8cee76ecbe | ||
|
|
6abcb6b69f | ||
|
|
075bd524a9 | ||
|
|
5a96c2d4d0 | ||
|
|
8de97ff483 | ||
|
|
b7449480b3 | ||
|
|
38f00302bf | ||
|
|
850fd440ff | ||
|
|
b243e2b51d | ||
|
|
34fc8fa8e5 | ||
|
|
6ceeae4d2b | ||
|
|
abd0427273 | ||
|
|
82f0c5d52e | ||
|
|
c7fa1efe5d | ||
|
|
be0571a67f | ||
|
|
a79d932801 | ||
|
|
8157ef050e | ||
|
|
04d1891891 | ||
|
|
1d7eeeba36 | ||
|
|
52f6e857df | ||
|
|
255fd4173d | ||
|
|
1a80a5ac07 | ||
|
|
6324dea166 | ||
|
|
695c438ec3 | ||
|
|
bb0b1187a0 | ||
|
|
d4f31efdea |
41
.github/workflows/slmcp-docker.yml
vendored
Normal file
41
.github/workflows/slmcp-docker.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: MCP Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*"
|
||||
paths:
|
||||
- "mcp_server/**"
|
||||
- ".github/workflows/slmcp-docker.yml"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERIO_USERNAME }}
|
||||
password: ${{ secrets.DOCKERIO_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./mcp_server
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
chaitin/safeline-mcp:latest
|
||||
chaitin/safeline-mcp:${{ github.ref_name }}
|
||||
cache-from: type=registry,ref=chaitin/safeline-mcp:buildcache
|
||||
cache-to: type=registry,ref=chaitin/safeline-mcp:buildcache,mode=max
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,3 +5,6 @@
|
||||
*.tar.gz
|
||||
build.sh
|
||||
compose.yml
|
||||
__pycache__
|
||||
.cursor
|
||||
.vscode
|
||||
10
README.md
10
README.md
@@ -7,9 +7,9 @@
|
||||
</h4>
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://waf.chaitin.com/">🏠 Website</a> |
|
||||
<a target="_blank" href="https://docs.waf.chaitin.com/">📖 Docs</a> |
|
||||
<a target="_blank" href="https://demo.waf.chaitin.com:9443/">🔍 Live Demo</a> |
|
||||
<a target="_blank" href="https://ly.safepoint.cloud/laA8asp">🏠 Website</a> |
|
||||
<a target="_blank" href="https://ly.safepoint.cloud/w2AeHhb">📖 Docs</a> |
|
||||
<a target="_blank" href="https://ly.safepoint.cloud/hSMd4SH">🔍 Live Demo</a> |
|
||||
<a target="_blank" href="https://discord.gg/SVnZGzHFvn">🙋♂️ Discord</a> |
|
||||
<a target="_blank" href="/README_CN.md">中文版</a>
|
||||
</p>
|
||||
@@ -77,11 +77,11 @@ List of the main features as follows:
|
||||
|
||||
#### 📦 Installing
|
||||
|
||||
Information on how to install SafeLine can be found in the [Install Guide](https://docs.waf.chaitin.com/en/tutorials/install)
|
||||
Information on how to install SafeLine can be found in the [Install Guide](https://docs.waf.chaitin.com/en/GetStarted/Deploy)
|
||||
|
||||
#### ⚙️ Protecting Web Apps
|
||||
|
||||
to see [Configuration](https://docs.waf.chaitin.com/en/tutorials/Configuration)
|
||||
to see [Configuration](https://docs.waf.chaitin.com/en/GetStarted/AddApplication)
|
||||
|
||||
## 📋 More Informations
|
||||
|
||||
|
||||
28
mcp_server/Dockerfile
Normal file
28
mcp_server/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 go build -o mcp-server .
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
COPY --from=builder /app/mcp-server .
|
||||
COPY --from=builder /app/config.yaml .
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
EXPOSE 5678
|
||||
|
||||
CMD ["./mcp-server"]
|
||||
256
mcp_server/README.md
Normal file
256
mcp_server/README.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# SafeLine MCP Server
|
||||
|
||||
SafeLine MCP Server is an implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) that provides complete management and control capabilities for SafeLine WAF.
|
||||
|
||||
[](docker-compose.yml)
|
||||
[](go.mod)
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Automated management and control of SafeLine WAF instances
|
||||
- WAF configuration and policy management through API
|
||||
- Building AI-based security protection tools and applications
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install [Docker](https://www.docker.com/) (if running in container)
|
||||
2. Configure SafeLine API Token (obtained from SafeLine console)
|
||||
|
||||
## Features
|
||||
|
||||
- Complete MCP (Management Control Protocol) server implementation
|
||||
- Support for SafeLine WAF instance management and control
|
||||
- Flexible configuration system supporting file configuration and environment variables
|
||||
- Docker containerization support
|
||||
- Secure API communication
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Environment Variable | Description | Default Value | Required |
|
||||
|---------|------|--------|-----|
|
||||
| LISTEN_PORT | Service listening port | 5678 | No |
|
||||
| LISTEN_ADDRESS | Service listening address | 0.0.0.0 | No |
|
||||
| SAFELINE_SECRET | SSE server secret | - | No |
|
||||
| SAFELINE_ADDRESS | SafeLine API address | - | Yes |
|
||||
| SAFELINE_API_TOKEN | SafeLine API authentication token | - | Yes |
|
||||
|
||||
### Using Docker
|
||||
|
||||
#### Method 1: Using docker run
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name safeline-mcp \
|
||||
-p 5678:5678 \
|
||||
-e SAFELINE_API_TOKEN="your_api_token" \
|
||||
-e SAFELINE_ADDRESS="https://your.safeline.com" \
|
||||
-e LISTEN_PORT=5678 \
|
||||
-e LISTEN_ADDRESS="0.0.0.0" \
|
||||
chaitin/safeline-mcp:latest
|
||||
```
|
||||
|
||||
#### Method 2: Using docker-compose
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone https://github.com/chaitin/safeline-mcp.git
|
||||
cd safeline-mcp
|
||||
|
||||
# 2. Edit docker-compose.yml to configure environment variables
|
||||
# Example docker-compose.yml:
|
||||
# version: '3'
|
||||
# services:
|
||||
# mcp:
|
||||
# image: chaitin/safeline-mcp:latest
|
||||
# container_name: safeline-mcp
|
||||
# ports:
|
||||
# - "5678:5678"
|
||||
# environment:
|
||||
# - SAFELINE_API_TOKEN=your_api_token
|
||||
# - SAFELINE_ADDRESS=https://your.safeline.com
|
||||
# - LISTEN_PORT=5678
|
||||
# - LISTEN_ADDRESS=0.0.0.0
|
||||
|
||||
# 3. Start service
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
#### Method 3: Using Go
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone https://github.com/chaitin/SafeLine.git
|
||||
cd safeline-mcp
|
||||
|
||||
# 2. Install dependencies
|
||||
go mod download
|
||||
|
||||
# 3. Configure config.yaml
|
||||
cp config.yaml.example config.yaml
|
||||
# Edit config.yaml with necessary configurations
|
||||
|
||||
# 4. Run service
|
||||
go run main.go
|
||||
```
|
||||
|
||||
For more API details, please refer to the [API Documentation](https://demo.waf.chaitin.com:9443/swagger/index.html).
|
||||
|
||||
## Tools
|
||||
|
||||
### Application Management
|
||||
|
||||
- **create_application**
|
||||
|
||||
### Rule Management
|
||||
- **create_blacklist_rule**
|
||||
- **create_whitelist_rule**
|
||||
|
||||
### Analyze
|
||||
- **get_attack_events**
|
||||
|
||||
## Development Guide
|
||||
|
||||
The Go API in this project is currently under development, and APIs may change. If you have specific requirements, please submit an Issue for discussion.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
internal/
|
||||
├── api/ # API implementation
|
||||
│ ├── app/ # Application-related APIs
|
||||
│ │ └── create_application.go
|
||||
│ └── rule/ # Rule-related APIs
|
||||
│ └── create_rule.go
|
||||
└── tools/ # MCP tool implementation
|
||||
├── app/ # Application-related tools
|
||||
│ └── create_application.go
|
||||
└── rule/ # Rule-related tools
|
||||
└── create_rule.go
|
||||
```
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
1. **Create Tool File**
|
||||
- Create corresponding directory and file under `internal/tools`
|
||||
- File name should match tool name
|
||||
- Use separate file for each tool
|
||||
- Example: `internal/tools/app/create_application.go`
|
||||
|
||||
2. **Tool Implementation Template**
|
||||
```go
|
||||
package app
|
||||
|
||||
type ToolName struct{}
|
||||
|
||||
type ToolParams struct {
|
||||
// Parameter definitions
|
||||
Param1 string `json:"param1" desc:"parameter description" required:"true"`
|
||||
Param2 int `json:"param2" desc:"parameter description" required:"false"`
|
||||
}
|
||||
|
||||
type ToolResult struct {
|
||||
Field1 string `json:"field1"`
|
||||
}
|
||||
|
||||
func (t *ToolName) Name() string {
|
||||
return "tool_name"
|
||||
}
|
||||
|
||||
func (t *ToolName) Description() string {
|
||||
return "tool description"
|
||||
}
|
||||
|
||||
func (t *ToolName) Validate(params ToolParams) error {
|
||||
// Parameter validation logic
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ToolName) Execute(ctx context.Context, params ToolParams) (result ToolResult, err error) {
|
||||
// Tool execution logic
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
3. **[Optional]Create API Implementation**
|
||||
|
||||
If you need to use some APIs that have not been implemented yet, you need to create corresponding files in the api directory for implementation
|
||||
- Create same directory structure under `internal/api`
|
||||
- File name should match tool func
|
||||
- Example: `internal/api/app/create_application.go`
|
||||
|
||||
**API Implementation Template**
|
||||
```go
|
||||
package app
|
||||
|
||||
type RequestType struct {
|
||||
// Request parameter definitions
|
||||
Param1 string `json:"param1"`
|
||||
Param2 int `json:"param2"`
|
||||
}
|
||||
|
||||
func APIName(ctx context.Context, req *RequestType) (ResultType, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("request is required")
|
||||
}
|
||||
|
||||
var resp api.Response[ResultType]
|
||||
err := api.Service().Post(ctx, "/api/path", req, &resp)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute")
|
||||
}
|
||||
|
||||
if resp.Err != nil {
|
||||
return nil, errors.New(resp.Msg)
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
}
|
||||
```
|
||||
4. **Tool Registration (init.go)**
|
||||
|
||||
The tool registration file `internal/tools/init.go` is used to centrally manage all tool registrations
|
||||
- Register all tools uniformly in the `init()` function
|
||||
- Use the `AppendTool()` method for registration
|
||||
- Example:
|
||||
```go
|
||||
// Register create application tool
|
||||
AppendTool(&app.CreateApp{})
|
||||
|
||||
// Register create blacklist rule tool
|
||||
AppendTool(&rule.CreateBlacklistRule{})
|
||||
```
|
||||
|
||||
### Development Standards
|
||||
|
||||
1. **Naming Conventions**
|
||||
- Use lowercase letters and underscores for tool names
|
||||
- File names should match tool names
|
||||
|
||||
2. **Directory Organization**
|
||||
- Divide directories by functional modules (e.g., app, rule, etc.)
|
||||
- Maintain consistent structure between tools and api directories
|
||||
- Keep related functionality in the same directory
|
||||
|
||||
3. **Code Standards**
|
||||
- Follow Go standard code conventions
|
||||
- Add necessary parameter validation
|
||||
- Use unified error handling approach
|
||||
- Add appropriate logging
|
||||
|
||||
4. **Documentation Requirements**
|
||||
- Provide clear functional description in tool Description
|
||||
- Add detailed description for parameters
|
||||
- Update API toolkit documentation in README
|
||||
|
||||
### Example
|
||||
|
||||
Refer to the implementation of the `create_application` tool:
|
||||
- Tool implementation: `internal/tools/app/create_application.go`
|
||||
- API implementation: `internal/api/app/create_application.go`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
27
mcp_server/config.yaml
Normal file
27
mcp_server/config.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# Server Configuration
|
||||
server:
|
||||
name: "SafeLine MCP Server"
|
||||
version: "1.0.0"
|
||||
# Can be overridden by environment variable LISTEN_PORT
|
||||
port: 5678
|
||||
# Can be overridden by environment variable LISTEN_ADDRESS
|
||||
host: "0.0.0.0"
|
||||
# Can be overridden by environment variable SAFELINE_SECRET
|
||||
secret: "" # Secret for SSE server
|
||||
# Logger Configuration
|
||||
logger:
|
||||
level: "info" # Log level: debug, info, warn, error
|
||||
file_path: "" # Log file path
|
||||
console: true # Whether to output to console
|
||||
caller: false # Whether to record caller information
|
||||
development: true # Whether to use development mode
|
||||
|
||||
# API Configuration
|
||||
api:
|
||||
# Can be overridden by environment variable SAFELINE_ADDRESS
|
||||
base_url: "" # API service address
|
||||
# Can be overridden by environment variable SAFELINE_API_TOKEN
|
||||
token: "" # Authentication token
|
||||
timeout: 30 # Timeout in seconds
|
||||
debug: false # Whether to enable debug mode
|
||||
insecure_skip_verify: true # Whether to skip certificate verification
|
||||
15
mcp_server/docker-compose.yml
Normal file
15
mcp_server/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcp_server:
|
||||
image: chaitin/safeline-mcp:latest
|
||||
container_name: mcp_server
|
||||
restart: always
|
||||
ports:
|
||||
- "5678:5678"
|
||||
environment:
|
||||
- SAFELINE_SECRET=your_secret_key # optional, if you want to use secret key to authenticate
|
||||
- SAFELINE_ADDRESS=https://your_safeline_ip:9443 # required, your SafeLine WAF address
|
||||
- SAFELINE_API_TOKEN=your_safeline_api_token # required, your SafeLine WAF api token
|
||||
- LISTEN_PORT=5678 # optional, default is 5678
|
||||
- LISTEN_ADDRESS=0.0.0.0 # optional, default is 0.0.0.0
|
||||
16
mcp_server/go.mod
Normal file
16
mcp_server/go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/chaitin/SafeLine/mcp_server
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/mark3labs/mcp-go v0.18.0
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mcuadros/go-defaults v1.2.0
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
)
|
||||
28
mcp_server/go.sum
Normal file
28
mcp_server/go.sum
Normal file
@@ -0,0 +1,28 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao=
|
||||
github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
|
||||
github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=
|
||||
github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
47
mcp_server/internal/api/analyze/get_event_list.go
Normal file
47
mcp_server/internal/api/analyze/get_event_list.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package analyze
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
)
|
||||
|
||||
type GetEventListRequest struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
IP string `json:"ip"`
|
||||
Start int64 `json:"start"`
|
||||
End int64 `json:"end"`
|
||||
}
|
||||
|
||||
type GetEventListResponse struct {
|
||||
Nodes []Event `json:"nodes"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
ID uint `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Protocol int `json:"protocol"`
|
||||
Host string `json:"host"`
|
||||
DstPort uint64 `json:"dst_port"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
StartAt int64 `json:"start_at"`
|
||||
EndAt int64 `json:"end_at"`
|
||||
DenyCount int64 `json:"deny_count"`
|
||||
PassCount int64 `json:"pass_count"`
|
||||
Finished bool `json:"finished"`
|
||||
Country string `json:"country"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
func GetEventList(ctx context.Context, req *GetEventListRequest) (*GetEventListResponse, error) {
|
||||
var resp api.Response[GetEventListResponse]
|
||||
err := api.Service().Get(ctx, fmt.Sprintf("/api/open/events?page=%d&page_size=%d&ip=%s&start=%d&end=%d", req.Page, req.PageSize, req.IP, req.Start, req.End), &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
34
mcp_server/internal/api/app/create_application.go
Normal file
34
mcp_server/internal/api/app/create_application.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
)
|
||||
|
||||
type CreateAppRequest struct {
|
||||
ServerNames []string `json:"server_names"`
|
||||
Ports []string `json:"ports"`
|
||||
Upstreams []string `json:"upstreams"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// CreateApp Create new website or app
|
||||
func CreateApp(ctx context.Context, req *CreateAppRequest) (int64, error) {
|
||||
if req == nil {
|
||||
return 0, errors.New("request is required")
|
||||
}
|
||||
|
||||
var resp api.Response[int64]
|
||||
err := api.Service().Post(ctx, "/api/open/site", req, &resp)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to create app")
|
||||
}
|
||||
|
||||
if resp.Err != nil {
|
||||
return 0, errors.New(resp.Msg)
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
}
|
||||
157
mcp_server/internal/api/client.go
Normal file
157
mcp_server/internal/api/client.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
// Client API client
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
// ClientOption Client configuration options
|
||||
type ClientOption func(*Client)
|
||||
|
||||
// WithTimeout Set timeout duration
|
||||
func WithTimeout(timeout time.Duration) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.httpClient.Timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeader Set request header
|
||||
func WithHeader(key, value string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.headers[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// WithBaseURL Set base URL
|
||||
func WithBaseURL(baseURL string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.baseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
// WithInsecureSkipVerify Set whether to skip certificate verification
|
||||
func WithInsecureSkipVerify(skip bool) ClientOption {
|
||||
return func(c *Client) {
|
||||
if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
|
||||
if transport.TLSClientConfig == nil {
|
||||
transport.TLSClientConfig = &tls.Config{}
|
||||
}
|
||||
transport.TLSClientConfig.InsecureSkipVerify = skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient Create new API client
|
||||
func NewClient(opts ...ClientOption) *Client {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{},
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: transport,
|
||||
},
|
||||
headers: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Request Send request
|
||||
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
|
||||
reqURL := fmt.Sprintf("%s%s", c.baseURL, path)
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshal request body failed")
|
||||
}
|
||||
bodyReader = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
logger.With("url", reqURL).Debug("request url")
|
||||
req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create request failed")
|
||||
}
|
||||
|
||||
// Set common headers
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
for k, v := range c.headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "send request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response body
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read response body failed")
|
||||
}
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return errors.New(fmt.Sprintf("request failed with status %d: %s", resp.StatusCode, string(respBody)))
|
||||
}
|
||||
|
||||
// Parse response
|
||||
if result != nil {
|
||||
if err := json.Unmarshal(respBody, result); err == nil {
|
||||
return nil
|
||||
}
|
||||
var respData map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &respData); err != nil {
|
||||
return errors.Wrap(err, "unmarshal response failed")
|
||||
}
|
||||
if respData["err"] != nil || respData["msg"] != nil {
|
||||
return errors.New(respData["msg"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get Send GET request
|
||||
func (c *Client) Get(ctx context.Context, path string, result interface{}) error {
|
||||
return c.Request(ctx, http.MethodGet, path, nil, result)
|
||||
}
|
||||
|
||||
// Post Send POST request
|
||||
func (c *Client) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
|
||||
return c.Request(ctx, http.MethodPost, path, body, result)
|
||||
}
|
||||
|
||||
// Put Send PUT request
|
||||
func (c *Client) Put(ctx context.Context, path string, body interface{}, result interface{}) error {
|
||||
return c.Request(ctx, http.MethodPut, path, body, result)
|
||||
}
|
||||
|
||||
// Delete Send DELETE request
|
||||
func (c *Client) Delete(ctx context.Context, path string, result interface{}) error {
|
||||
return c.Request(ctx, http.MethodDelete, path, nil, result)
|
||||
}
|
||||
11
mcp_server/internal/api/response.go
Normal file
11
mcp_server/internal/api/response.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package api
|
||||
|
||||
// Response Common API response structure
|
||||
type Response[T any] struct {
|
||||
// Response data
|
||||
Data T `json:"data"`
|
||||
// Error message
|
||||
Err any `json:"err"`
|
||||
// Prompt message
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
35
mcp_server/internal/api/rule/create_rule.go
Normal file
35
mcp_server/internal/api/rule/create_rule.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
)
|
||||
|
||||
type CreateRuleRequest struct {
|
||||
Name string `json:"name"`
|
||||
IP []string `json:"ip"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
Pattern [][]api.Pattern `json:"pattern"`
|
||||
Action int `json:"action"`
|
||||
}
|
||||
|
||||
// CreateRule Create new rule
|
||||
func CreateRule(ctx context.Context, req *CreateRuleRequest) (int64, error) {
|
||||
if req == nil {
|
||||
return 0, errors.New("request is required")
|
||||
}
|
||||
|
||||
var resp api.Response[int64]
|
||||
err := api.Service().Post(ctx, "/api/open/policy", req, &resp)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to create policy rule")
|
||||
}
|
||||
|
||||
if resp.Err != nil {
|
||||
return 0, errors.New(resp.Msg)
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
}
|
||||
100
mcp_server/internal/api/service.go
Normal file
100
mcp_server/internal/api/service.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/config"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
// APIClient API client implementation
|
||||
type APIClient struct {
|
||||
client *Client
|
||||
config *config.APIConfig
|
||||
}
|
||||
|
||||
var (
|
||||
instance *APIClient
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Init Initialize API service
|
||||
func Init(cfg *config.APIConfig) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
instance, err = newAPIClient(cfg)
|
||||
if err != nil {
|
||||
logger.With("error", err).Error("failed to initialize API service")
|
||||
return
|
||||
}
|
||||
logger.Info("API service initialized successfully")
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Service Get API service instance
|
||||
func Service() *APIClient {
|
||||
if instance == nil {
|
||||
logger.Error("API service not initialized")
|
||||
panic("API service not initialized")
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
// newAPIClient Create new API client
|
||||
func newAPIClient(config *config.APIConfig) (*APIClient, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is required")
|
||||
}
|
||||
|
||||
if config.BaseURL == "" {
|
||||
return nil, errors.New("base_url is required")
|
||||
}
|
||||
|
||||
timeout := 30
|
||||
if config.Timeout > 0 {
|
||||
timeout = config.Timeout
|
||||
}
|
||||
|
||||
opts := []ClientOption{
|
||||
WithBaseURL(config.BaseURL),
|
||||
WithTimeout(time.Duration(timeout) * time.Second),
|
||||
WithHeader("User-Agent", "SafeLine-MCP/1.0"),
|
||||
WithInsecureSkipVerify(config.InsecureSkipVerify),
|
||||
}
|
||||
|
||||
// If token is configured, add authentication header
|
||||
if config.Token != "" {
|
||||
opts = append(opts, WithHeader("X-SLCE-API-TOKEN", config.Token))
|
||||
}
|
||||
|
||||
client := NewClient(opts...)
|
||||
|
||||
return &APIClient{
|
||||
client: client,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Post Send POST request
|
||||
func (c *APIClient) Post(ctx context.Context, path string, body interface{}, result interface{}) error {
|
||||
return c.client.Request(ctx, "POST", path, body, result)
|
||||
}
|
||||
|
||||
// Get Send GET request
|
||||
func (c *APIClient) Get(ctx context.Context, path string, result interface{}) error {
|
||||
return c.client.Request(ctx, "GET", path, nil, result)
|
||||
}
|
||||
|
||||
// Put Send PUT request
|
||||
func (c *APIClient) Put(ctx context.Context, path string, body interface{}, result interface{}) error {
|
||||
return c.client.Request(ctx, "PUT", path, body, result)
|
||||
}
|
||||
|
||||
// Delete Send DELETE request
|
||||
func (c *APIClient) Delete(ctx context.Context, path string, result interface{}) error {
|
||||
return c.client.Request(ctx, "DELETE", path, nil, result)
|
||||
}
|
||||
50
mcp_server/internal/api/types.go
Normal file
50
mcp_server/internal/api/types.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package api
|
||||
|
||||
type PolicyRuleAction int
|
||||
|
||||
const (
|
||||
PolicyRuleActionAllow PolicyRuleAction = iota
|
||||
PolicyRuleActionDeny
|
||||
PolicyRuleActionMax
|
||||
)
|
||||
|
||||
type Key = string
|
||||
|
||||
const (
|
||||
KeySrcIP Key = "src_ip"
|
||||
KeyURI Key = "uri"
|
||||
KeyURINoQuery Key = "uri_no_query"
|
||||
KeyHost Key = "host"
|
||||
KeyMethod Key = "method"
|
||||
KeyReqHeader Key = "req_header"
|
||||
KeyReqBody Key = "req_body"
|
||||
KeyGetParam Key = "get_param"
|
||||
KeyPostParam Key = "post_param"
|
||||
)
|
||||
|
||||
type Op = string
|
||||
|
||||
const (
|
||||
OpEq Op = "eq" // equal
|
||||
OpNotEq Op = "not_eq" // not equal
|
||||
OpMatch Op = "match" // match
|
||||
OpCIDR Op = "cidr" // cidr
|
||||
OpHas Op = "has" // has
|
||||
OpNotHas Op = "not_has" // not has
|
||||
OpPrefix Op = "prefix" // prefix
|
||||
OpRe Op = "re" // regex
|
||||
OpIn Op = "in" // in
|
||||
OpNotIn Op = "not_in" // not in
|
||||
OpNotCIDR Op = "not_cidr" // not cidr
|
||||
OpExist Op = "exist" // exist
|
||||
OpNotExist Op = "not_exist" // not exist
|
||||
OpGeoEq Op = "geo_eq" // geo equal
|
||||
OpGeoNotEq Op = "geo_not_eq" // geo not equal
|
||||
)
|
||||
|
||||
type Pattern struct {
|
||||
K Key `json:"k"`
|
||||
Op Op `json:"op"`
|
||||
V []string `json:"v"`
|
||||
SubK string `json:"sub_k"`
|
||||
}
|
||||
118
mcp_server/internal/config/config.go
Normal file
118
mcp_server/internal/config/config.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config Global configuration structure
|
||||
type Config struct {
|
||||
Server *ServerConfig `yaml:"server"`
|
||||
Logger *LoggerConfig `yaml:"logger"`
|
||||
API *APIConfig `yaml:"api"`
|
||||
}
|
||||
|
||||
// APIConfig API configuration
|
||||
type APIConfig struct {
|
||||
// API base URL
|
||||
BaseURL string `yaml:"base_url"`
|
||||
// API token
|
||||
Token string `yaml:"token"`
|
||||
// API timeout
|
||||
Timeout int `yaml:"timeout"`
|
||||
// API debug mode
|
||||
Debug bool `yaml:"debug"`
|
||||
// API insecure skip verify
|
||||
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
|
||||
}
|
||||
|
||||
// ServerConfig Server configuration
|
||||
type ServerConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
Port int `yaml:"port"`
|
||||
Host string `yaml:"host"`
|
||||
Secret string `yaml:"secret"`
|
||||
}
|
||||
|
||||
// LoggerConfig Logger configuration
|
||||
type LoggerConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
FilePath string `yaml:"file_path"`
|
||||
Console bool `yaml:"console"`
|
||||
Caller bool `yaml:"caller"`
|
||||
Development bool `yaml:"development"`
|
||||
}
|
||||
|
||||
var config *Config
|
||||
|
||||
// getEnvString Get string value from environment variable, return default value if not exists
|
||||
func getEnvString(key, defaultValue string) string {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getEnvInt Get integer value from environment variable, return default value if not exists or cannot be parsed
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Load Load configuration file
|
||||
func Load(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read config file failed")
|
||||
}
|
||||
|
||||
config = &Config{}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return errors.Wrap(err, "unmarshal config failed")
|
||||
}
|
||||
|
||||
// Override configuration from environment variables
|
||||
if config.Server != nil {
|
||||
config.Server.Host = getEnvString("LISTEN_ADDRESS", config.Server.Host)
|
||||
config.Server.Port = getEnvInt("LISTEN_PORT", config.Server.Port)
|
||||
}
|
||||
|
||||
if config.API != nil {
|
||||
config.API.BaseURL = getEnvString("SAFELINE_ADDRESS", config.API.BaseURL)
|
||||
config.API.Token = getEnvString("SAFELINE_API_TOKEN", config.API.Token)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServer Get server configuration
|
||||
func GetServer() *ServerConfig {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.Server
|
||||
}
|
||||
|
||||
// GetLogger Get logger configuration
|
||||
func GetLogger() *LoggerConfig {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.Logger
|
||||
}
|
||||
|
||||
// GetAPI Get API configuration
|
||||
func GetAPI() *APIConfig {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.API
|
||||
}
|
||||
45
mcp_server/internal/tools/analyze/get_atttack_events.go
Normal file
45
mcp_server/internal/tools/analyze/get_atttack_events.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package analyze
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api/analyze"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type GetAttackEventsParams struct {
|
||||
IP string `json:"ip" desc:"ip" required:"false"`
|
||||
Page int `json:"page" desc:"page" required:"false" default:"1"`
|
||||
PageSize int `json:"page_size" desc:"page size" required:"false" default:"10"`
|
||||
Start int64 `json:"start" desc:"start unix timestamp in milliseconds" required:"false"`
|
||||
End int64 `json:"end" desc:"end unix timestamp in milliseconds" required:"false"`
|
||||
}
|
||||
|
||||
type GetAttackEvents struct{}
|
||||
|
||||
func (t *GetAttackEvents) Name() string {
|
||||
return "get_attack_events"
|
||||
}
|
||||
|
||||
func (t *GetAttackEvents) Description() string {
|
||||
return "get attack events"
|
||||
}
|
||||
|
||||
func (t *GetAttackEvents) Validate(params GetAttackEventsParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *GetAttackEvents) Execute(ctx context.Context, params GetAttackEventsParams) (analyze.GetEventListResponse, error) {
|
||||
resp, err := analyze.GetEventList(ctx, &analyze.GetEventListRequest{
|
||||
IP: params.IP,
|
||||
PageSize: params.PageSize,
|
||||
Page: params.Page,
|
||||
Start: params.Start,
|
||||
End: params.End,
|
||||
})
|
||||
if err != nil {
|
||||
return analyze.GetEventListResponse{}, err
|
||||
}
|
||||
logger.With("total", resp.Total).Info("get attack events")
|
||||
return *resp, nil
|
||||
}
|
||||
41
mcp_server/internal/tools/app/create_application.go
Normal file
41
mcp_server/internal/tools/app/create_application.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api/app"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type CreateApp struct{}
|
||||
|
||||
type CreateAppParams struct {
|
||||
ServerNames []string `json:"server_names" desc:"domain list" required:"true"`
|
||||
Ports []string `json:"ports" desc:"port list" required:"true"`
|
||||
Upstreams []string `json:"upstreams" desc:"upstream list" required:"true"`
|
||||
}
|
||||
|
||||
func (t *CreateApp) Name() string {
|
||||
return "create_http_application"
|
||||
}
|
||||
|
||||
func (t *CreateApp) Description() string {
|
||||
return "create a new website or app"
|
||||
}
|
||||
|
||||
func (t *CreateApp) Validate(params CreateAppParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CreateApp) Execute(ctx context.Context, params CreateAppParams) (int64, error) {
|
||||
id, err := app.CreateApp(ctx, &app.CreateAppRequest{
|
||||
ServerNames: params.ServerNames,
|
||||
Ports: params.Ports,
|
||||
Upstreams: params.Upstreams,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logger.Info("create app success", logger.Int64("id", id))
|
||||
return id, nil
|
||||
}
|
||||
46
mcp_server/internal/tools/example.go
Normal file
46
mcp_server/internal/tools/example.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type CalculateSum struct{}
|
||||
|
||||
func (t *CalculateSum) Name() string {
|
||||
return "calculate_sum"
|
||||
}
|
||||
|
||||
func (t *CalculateSum) Description() string {
|
||||
return "Add two numbers together"
|
||||
}
|
||||
|
||||
type MyToolInput struct {
|
||||
A int `json:"a" desc:"number a" required:"true"`
|
||||
B int `json:"b" desc:"number b" required:"true"`
|
||||
}
|
||||
|
||||
type MyToolOutput struct {
|
||||
C int `json:"c"`
|
||||
}
|
||||
|
||||
func (t *CalculateSum) Validate(params MyToolInput) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CalculateSum) Execute(ctx context.Context, params MyToolInput) (MyToolOutput, error) {
|
||||
logger.With("a", params.A).
|
||||
With("b", params.B).
|
||||
Debug("Executing calculation")
|
||||
|
||||
result := MyToolOutput{
|
||||
C: params.A + params.B,
|
||||
}
|
||||
|
||||
logger.With("result", result.C).
|
||||
Debug("Calculation completed")
|
||||
|
||||
return result, errors.New("test error")
|
||||
}
|
||||
19
mcp_server/internal/tools/init.go
Normal file
19
mcp_server/internal/tools/init.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/tools/analyze"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/tools/app"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/tools/rule"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// app
|
||||
AppendTool(&app.CreateApp{})
|
||||
|
||||
// rule
|
||||
AppendTool(&rule.CreateBlacklistRule{})
|
||||
AppendTool(&rule.CreateWhitelistRule{})
|
||||
|
||||
// analyze
|
||||
AppendTool(&analyze.GetAttackEvents{})
|
||||
}
|
||||
66
mcp_server/internal/tools/rule/create_blacklist_rule.go
Normal file
66
mcp_server/internal/tools/rule/create_blacklist_rule.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api/rule"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type CreateBlacklistRule struct{}
|
||||
|
||||
type CreateBlacklistRuleParams struct {
|
||||
Name string `json:"name" desc:"name" required:"true"`
|
||||
IP []string `json:"ip" desc:"ip" required:"false"`
|
||||
URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"`
|
||||
}
|
||||
|
||||
func (t *CreateBlacklistRule) Name() string {
|
||||
return "create_blacklist_rule"
|
||||
}
|
||||
|
||||
func (t *CreateBlacklistRule) Description() string {
|
||||
return "create a new blacklist rule"
|
||||
}
|
||||
|
||||
func (t *CreateBlacklistRule) Validate(params CreateBlacklistRuleParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CreateBlacklistRule) Execute(ctx context.Context, params CreateBlacklistRuleParams) (int64, error) {
|
||||
var pattern [][]api.Pattern
|
||||
if len(params.IP) > 0 {
|
||||
pattern = append(pattern, []api.Pattern{
|
||||
{
|
||||
K: api.KeySrcIP,
|
||||
Op: api.OpEq,
|
||||
V: params.IP,
|
||||
SubK: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(params.URINoQuery) > 0 {
|
||||
pattern = append(pattern, []api.Pattern{
|
||||
{
|
||||
K: api.KeyURINoQuery,
|
||||
Op: api.OpEq,
|
||||
V: params.URINoQuery,
|
||||
SubK: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{
|
||||
Name: params.Name,
|
||||
IP: params.IP,
|
||||
IsEnabled: true,
|
||||
Action: int(api.PolicyRuleActionDeny),
|
||||
Pattern: pattern,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logger.With("id", id).Info("create blacklist rule success")
|
||||
return id, nil
|
||||
}
|
||||
66
mcp_server/internal/tools/rule/create_whitelist_rule.go
Normal file
66
mcp_server/internal/tools/rule/create_whitelist_rule.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api/rule"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
type CreateWhitelistRule struct{}
|
||||
|
||||
type CreateWhitelistRuleParams struct {
|
||||
Name string `json:"name" desc:"name" required:"true"`
|
||||
IP []string `json:"ip" desc:"ip" required:"false"`
|
||||
URINoQuery []string `json:"uri_no_query" desc:"uri_no_query" required:"false"`
|
||||
}
|
||||
|
||||
func (t *CreateWhitelistRule) Name() string {
|
||||
return "create_whitelist_rule"
|
||||
}
|
||||
|
||||
func (t *CreateWhitelistRule) Description() string {
|
||||
return "create a new whitelist rule"
|
||||
}
|
||||
|
||||
func (t *CreateWhitelistRule) Validate(params CreateWhitelistRuleParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CreateWhitelistRule) Execute(ctx context.Context, params CreateWhitelistRuleParams) (int64, error) {
|
||||
var pattern [][]api.Pattern
|
||||
if len(params.IP) > 0 {
|
||||
pattern = append(pattern, []api.Pattern{
|
||||
{
|
||||
K: api.KeySrcIP,
|
||||
Op: api.OpEq,
|
||||
V: params.IP,
|
||||
SubK: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(params.URINoQuery) > 0 {
|
||||
pattern = append(pattern, []api.Pattern{
|
||||
{
|
||||
K: api.KeyURINoQuery,
|
||||
Op: api.OpEq,
|
||||
V: params.URINoQuery,
|
||||
SubK: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
id, err := rule.CreateRule(ctx, &rule.CreateRuleRequest{
|
||||
Name: params.Name,
|
||||
IP: params.IP,
|
||||
IsEnabled: true,
|
||||
Action: int(api.PolicyRuleActionAllow),
|
||||
Pattern: pattern,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logger.With("id", id).Info("create whitelist rule success")
|
||||
return id, nil
|
||||
}
|
||||
41
mcp_server/internal/tools/tool.go
Normal file
41
mcp_server/internal/tools/tool.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/mcp"
|
||||
)
|
||||
|
||||
// By deferring the concretization of generic types to the Register method,
|
||||
// we avoid type inference issues.
|
||||
|
||||
// Each Tool is wrapped in a toolWrapper that knows its concrete type,
|
||||
// allowing correct passing of generic parameters during registration.
|
||||
type ToolWrapper interface {
|
||||
Register(s *mcp.MCPServer) error
|
||||
}
|
||||
|
||||
var (
|
||||
tools = []ToolWrapper{}
|
||||
)
|
||||
|
||||
func AppendTool[T any, R any](tool ...mcp.Tool[T, R]) {
|
||||
for _, t := range tool {
|
||||
tools = append(tools, &toolWrapper[T, R]{tool: t})
|
||||
}
|
||||
}
|
||||
|
||||
func Tools() []ToolWrapper {
|
||||
return tools
|
||||
}
|
||||
|
||||
type toolWrapper[T any, R any] struct {
|
||||
tool mcp.Tool[T, R]
|
||||
}
|
||||
|
||||
func (w *toolWrapper[T, R]) Register(s *mcp.MCPServer) error {
|
||||
logger.Info("Registering tool",
|
||||
logger.String("name", w.tool.Name()),
|
||||
logger.String("description", w.tool.Description()),
|
||||
)
|
||||
return mcp.RegisterTool(s, w.tool)
|
||||
}
|
||||
62
mcp_server/main.go
Normal file
62
mcp_server/main.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/api"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/config"
|
||||
"github.com/chaitin/SafeLine/mcp_server/internal/tools"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/mcp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.yaml", "path to config file")
|
||||
flag.Parse()
|
||||
|
||||
if err := config.Load(*configPath); err != nil {
|
||||
panic(fmt.Errorf("failed to load config: %v", err))
|
||||
}
|
||||
|
||||
logConfig := config.GetLogger()
|
||||
if err := logger.Init(&logger.Config{
|
||||
Level: logConfig.Level,
|
||||
FilePath: logConfig.FilePath,
|
||||
Console: logConfig.Console,
|
||||
Caller: logConfig.Caller,
|
||||
Development: logConfig.Development,
|
||||
}); err != nil {
|
||||
panic(fmt.Errorf("failed to init logger: %v", err))
|
||||
}
|
||||
|
||||
logger.With("base_url", config.GetAPI().BaseURL).Info("Initializing API service...")
|
||||
if err := api.Init(config.GetAPI()); err != nil {
|
||||
panic(fmt.Errorf("failed to init API service: %v", err))
|
||||
}
|
||||
|
||||
logger.Info("Starting MCP Server...")
|
||||
serverConfig := config.GetServer()
|
||||
s := mcp.NewMCPServer(
|
||||
serverConfig.Name,
|
||||
serverConfig.Version,
|
||||
serverConfig.Secret,
|
||||
)
|
||||
|
||||
logger.Info("Registering tools...")
|
||||
for _, tool := range tools.Tools() {
|
||||
if err := tool.Register(s); err != nil {
|
||||
logger.With("error", err).
|
||||
Error("Failed to register tool")
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", serverConfig.Host, serverConfig.Port)
|
||||
logger.With("addr", addr).Info("Starting server")
|
||||
if err := s.Start(addr); err != nil {
|
||||
logger.With("error", err).
|
||||
Error("Server failed to start")
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
72
mcp_server/pkg/config/config.go
Normal file
72
mcp_server/pkg/config/config.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config Global configuration structure
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Logger LoggerConfig `yaml:"logger"`
|
||||
}
|
||||
|
||||
// ServerConfig Server configuration
|
||||
type ServerConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
Port int `yaml:"port"`
|
||||
Host string `yaml:"host"`
|
||||
Secret string `yaml:"secret"`
|
||||
}
|
||||
|
||||
// LoggerConfig Logger configuration
|
||||
type LoggerConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
FilePath string `yaml:"file_path"`
|
||||
Console bool `yaml:"console"`
|
||||
Caller bool `yaml:"caller"`
|
||||
Development bool `yaml:"development"`
|
||||
}
|
||||
|
||||
var (
|
||||
globalConfig *Config
|
||||
)
|
||||
|
||||
// Load Load configuration from file
|
||||
func Load(filename string) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
globalConfig = config
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get Get global configuration
|
||||
func Get() *Config {
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
// GetServer Get server configuration
|
||||
func GetServer() ServerConfig {
|
||||
if globalConfig == nil {
|
||||
return ServerConfig{}
|
||||
}
|
||||
return globalConfig.Server
|
||||
}
|
||||
|
||||
// GetLogger Get logger configuration
|
||||
func GetLogger() LoggerConfig {
|
||||
if globalConfig == nil {
|
||||
return LoggerConfig{}
|
||||
}
|
||||
return globalConfig.Logger
|
||||
}
|
||||
136
mcp_server/pkg/errors/errors.go
Normal file
136
mcp_server/pkg/errors/errors.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// Common errors
|
||||
ErrInternal = New("internal error")
|
||||
ErrInvalidParam = New("invalid parameter")
|
||||
ErrNotFound = New("resource not found")
|
||||
ErrUnauthorized = New("unauthorized")
|
||||
ErrForbidden = New("forbidden")
|
||||
ErrTimeout = New("timeout")
|
||||
)
|
||||
|
||||
// Error Custom error structure
|
||||
type Error struct {
|
||||
err error
|
||||
stack []string
|
||||
msg string
|
||||
location string
|
||||
}
|
||||
|
||||
// Error Implement error interface
|
||||
func (e *Error) Error() string {
|
||||
if e.msg != "" {
|
||||
return fmt.Sprintf("%s: %v (at %s)", e.msg, e.err, e.location)
|
||||
}
|
||||
return fmt.Sprintf("%v (at %s)", e.err, e.location)
|
||||
}
|
||||
|
||||
// Unwrap Return original error
|
||||
func (e *Error) Unwrap() error {
|
||||
if e.err == nil {
|
||||
return nil
|
||||
}
|
||||
if wrapped, ok := e.err.(*Error); ok {
|
||||
return wrapped.Unwrap()
|
||||
}
|
||||
return e.err
|
||||
}
|
||||
|
||||
// Stack Return error stack
|
||||
func (e *Error) Stack() []string {
|
||||
return e.stack
|
||||
}
|
||||
|
||||
// Location Return error location
|
||||
func (e *Error) Location() string {
|
||||
return e.location
|
||||
}
|
||||
|
||||
// getCallerLocation Get caller location
|
||||
func getCallerLocation(skip int) string {
|
||||
_, file, line, ok := runtime.Caller(skip)
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
|
||||
// WrapL Wrap error and print log
|
||||
func WrapL(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get stack trace information
|
||||
var stack []string
|
||||
for i := 1; i < 32; i++ {
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
break
|
||||
}
|
||||
name := fn.Name()
|
||||
if strings.Contains(name, "runtime.") {
|
||||
break
|
||||
}
|
||||
stack = append(stack, fmt.Sprintf("%s:%d", file, line))
|
||||
}
|
||||
|
||||
wrappedErr := &Error{
|
||||
err: err,
|
||||
stack: stack,
|
||||
msg: msg,
|
||||
location: getCallerLocation(2),
|
||||
}
|
||||
|
||||
// Print error information and stack using logger
|
||||
logger.With("error", err).
|
||||
With("location", wrappedErr.location).
|
||||
With("stack", strings.Join(stack, "\n")).
|
||||
Error(msg)
|
||||
|
||||
return wrappedErr
|
||||
}
|
||||
|
||||
// Is Check error type
|
||||
func Is(err, target error) bool {
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
// As Type assertion
|
||||
func As(err error, target interface{}) bool {
|
||||
return errors.As(err, target)
|
||||
}
|
||||
|
||||
// Wrap Wrap error without printing log
|
||||
func Wrap(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &Error{
|
||||
err: err,
|
||||
msg: msg,
|
||||
location: getCallerLocation(2),
|
||||
}
|
||||
}
|
||||
|
||||
// New Create new error
|
||||
func New(text string) error {
|
||||
return &Error{
|
||||
err: errors.New(text),
|
||||
location: getCallerLocation(2),
|
||||
}
|
||||
}
|
||||
49
mcp_server/pkg/logger/field.go
Normal file
49
mcp_server/pkg/logger/field.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Field 日志字段
|
||||
type Field = zapcore.Field
|
||||
|
||||
// String 创建字符串字段
|
||||
func String(key string, val string) Field {
|
||||
return zap.String(key, val)
|
||||
}
|
||||
|
||||
// Int 创建整数字段
|
||||
func Int(key string, val int) Field {
|
||||
return zap.Int(key, val)
|
||||
}
|
||||
|
||||
// Int64 创建 int64 字段
|
||||
func Int64(key string, val int64) Field {
|
||||
return zap.Int64(key, val)
|
||||
}
|
||||
|
||||
// Float64 创建浮点数字段
|
||||
func Float64(key string, val float64) Field {
|
||||
return zap.Float64(key, val)
|
||||
}
|
||||
|
||||
// Bool 创建布尔字段
|
||||
func Bool(key string, val bool) Field {
|
||||
return zap.Bool(key, val)
|
||||
}
|
||||
|
||||
// Err 创建错误字段
|
||||
func Err(err error) Field {
|
||||
return zap.Error(err)
|
||||
}
|
||||
|
||||
// Any 创建任意类型字段
|
||||
func Any(key string, val interface{}) Field {
|
||||
return zap.Any(key, val)
|
||||
}
|
||||
|
||||
// Duration 创建时间段字段
|
||||
func Duration(key string, val float64) Field {
|
||||
return zap.Float64(key, val)
|
||||
}
|
||||
205
mcp_server/pkg/logger/logger.go
Normal file
205
mcp_server/pkg/logger/logger.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Logger 封装 zap.Logger
|
||||
type Logger struct {
|
||||
zl *zap.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
defaultLogger *Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Config 日志配置
|
||||
type Config struct {
|
||||
// 日志级别
|
||||
Level string `json:"level" yaml:"level"`
|
||||
// 日志文件路径
|
||||
FilePath string `json:"file_path" yaml:"file_path"`
|
||||
// 是否输出到控制台
|
||||
Console bool `json:"console" yaml:"console"`
|
||||
// 是否记录调用者信息
|
||||
Caller bool `json:"caller" yaml:"caller"`
|
||||
// 是否使用开发模式(更详细的日志)
|
||||
Development bool `json:"development" yaml:"development"`
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var defaultConfig = Config{
|
||||
Level: "info",
|
||||
FilePath: "logs/mcp.log",
|
||||
Console: true,
|
||||
Caller: true,
|
||||
Development: false,
|
||||
}
|
||||
|
||||
// Init 初始化日志系统
|
||||
func Init(cfg *Config) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
if cfg == nil {
|
||||
cfg = &defaultConfig
|
||||
}
|
||||
|
||||
// 确保日志目录存在
|
||||
if cfg.FilePath != "" {
|
||||
dir := filepath.Dir(cfg.FilePath)
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 配置编码器
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
|
||||
},
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
// 设置日志级别
|
||||
var level zapcore.Level
|
||||
if err = level.UnmarshalText([]byte(cfg.Level)); err != nil {
|
||||
level = zapcore.InfoLevel
|
||||
}
|
||||
|
||||
// 创建Core
|
||||
var cores []zapcore.Core
|
||||
|
||||
// 文件输出
|
||||
if cfg.FilePath != "" {
|
||||
fileWriter, err := os.OpenFile(cfg.FilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cores = append(cores, zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig),
|
||||
zapcore.AddSync(fileWriter),
|
||||
level,
|
||||
))
|
||||
}
|
||||
|
||||
// 控制台输出
|
||||
if cfg.Console {
|
||||
cores = append(cores, zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(encoderConfig),
|
||||
zapcore.AddSync(os.Stdout),
|
||||
level,
|
||||
))
|
||||
}
|
||||
|
||||
// 创建Logger
|
||||
core := zapcore.NewTee(cores...)
|
||||
zl := zap.New(core)
|
||||
|
||||
// 是否记录调用者信息
|
||||
if cfg.Caller {
|
||||
zl = zl.WithOptions(zap.AddCaller())
|
||||
}
|
||||
|
||||
// 是否使用开发模式
|
||||
if cfg.Development {
|
||||
zl = zl.WithOptions(zap.Development())
|
||||
}
|
||||
|
||||
defaultLogger = &Logger{zl: zl}
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLogger 获取日志实例
|
||||
func GetLogger() *Logger {
|
||||
if defaultLogger == nil {
|
||||
Init(nil)
|
||||
}
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
// With 创建带有字段的新Logger
|
||||
func (l *Logger) With(key string, value interface{}) *Logger {
|
||||
return &Logger{zl: l.zl.With(Any(key, value))}
|
||||
}
|
||||
|
||||
// Debug level
|
||||
func (l *Logger) Debug(msg string, fields ...Field) {
|
||||
l.zl.Debug(msg, fields...)
|
||||
}
|
||||
|
||||
// Info level
|
||||
func (l *Logger) Info(msg string, fields ...Field) {
|
||||
l.zl.Info(msg, fields...)
|
||||
}
|
||||
|
||||
// Warn level
|
||||
func (l *Logger) Warn(msg string, fields ...Field) {
|
||||
l.zl.Warn(msg, fields...)
|
||||
}
|
||||
|
||||
// Error level
|
||||
func (l *Logger) Error(msg string, fields ...Field) {
|
||||
l.zl.Error(msg, fields...)
|
||||
}
|
||||
|
||||
// Fatal level
|
||||
func (l *Logger) Fatal(msg string, fields ...Field) {
|
||||
l.zl.Fatal(msg, fields...)
|
||||
}
|
||||
|
||||
// 全局函数
|
||||
|
||||
// With 创建带有字段的新Logger
|
||||
func With(key string, value interface{}) *Logger {
|
||||
l := GetLogger()
|
||||
// 为链式调用创建新的logger实例,并添加caller skip
|
||||
return &Logger{zl: l.zl.WithOptions(zap.AddCallerSkip(1)).With(Any(key, value))}
|
||||
}
|
||||
|
||||
// Debug level
|
||||
func Debug(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Debug(msg, fields...)
|
||||
}
|
||||
|
||||
// Info level
|
||||
func Info(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Info(msg, fields...)
|
||||
}
|
||||
|
||||
// Warn level
|
||||
func Warn(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Warn(msg, fields...)
|
||||
}
|
||||
|
||||
// Error level
|
||||
func Error(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Error(msg, fields...)
|
||||
}
|
||||
|
||||
// Fatal level
|
||||
func Fatal(msg string, fields ...Field) {
|
||||
l := GetLogger()
|
||||
l.zl.WithOptions(zap.AddCallerSkip(1)).Fatal(msg, fields...)
|
||||
}
|
||||
148
mcp_server/pkg/mcp/mcp.go
Normal file
148
mcp_server/pkg/mcp/mcp.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/errors"
|
||||
"github.com/chaitin/SafeLine/mcp_server/pkg/logger"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/mcuadros/go-defaults"
|
||||
)
|
||||
|
||||
type Tool[T any, R any] interface {
|
||||
Name() string
|
||||
|
||||
Description() string
|
||||
|
||||
Execute(ctx context.Context, params T) (R, error)
|
||||
|
||||
Validate(params T) error
|
||||
}
|
||||
|
||||
type SSEServer struct {
|
||||
sse *server.SSEServer
|
||||
secret string
|
||||
}
|
||||
|
||||
func (s *SSEServer) Start(addr string) error {
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: s,
|
||||
}
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *SSEServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if s.secret == "" {
|
||||
s.sse.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
messagePath := s.sse.CompleteMessagePath()
|
||||
if messagePath != "" && r.URL.Path == messagePath {
|
||||
secret := r.Header.Get("Secret")
|
||||
if secret != s.secret {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
s.sse.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type MCPServer struct {
|
||||
server *server.MCPServer
|
||||
sse *SSEServer
|
||||
}
|
||||
|
||||
func NewMCPServer(name, version string, secret string) *MCPServer {
|
||||
s := server.NewMCPServer(
|
||||
name,
|
||||
version,
|
||||
server.WithLogging(),
|
||||
)
|
||||
return &MCPServer{
|
||||
server: s,
|
||||
sse: &SSEServer{sse: server.NewSSEServer(s), secret: secret},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) Start(addr string) error {
|
||||
return s.sse.Start(addr)
|
||||
}
|
||||
|
||||
func handleToolCall[T any, R any](ctx context.Context, request mcp.CallToolRequest, tool Tool[T, R]) (result *mcp.CallToolResult, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
var raw []byte
|
||||
raw, err = json.Marshal(request.Params.Arguments)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "marshal arguments failed")
|
||||
}
|
||||
var params T
|
||||
defaults.SetDefaults(¶ms)
|
||||
if err = json.Unmarshal(raw, ¶ms); err != nil {
|
||||
return nil, errors.Wrap(err, "unmarshal parameters failed")
|
||||
}
|
||||
|
||||
if err = tool.Validate(params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var execResult R
|
||||
execResult, err = tool.Execute(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := any(execResult)
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return mcp.NewToolResultText(v), nil
|
||||
case []byte:
|
||||
return mcp.NewToolResultText(string(v)), nil
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return mcp.NewToolResultText(json.Number(fmt.Sprint(v)).String()), nil
|
||||
case bool:
|
||||
return mcp.NewToolResultText(strconv.FormatBool(v)), nil
|
||||
default:
|
||||
bytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid result type")
|
||||
}
|
||||
return mcp.NewToolResultText(string(bytes)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterTool[T any, R any](s *MCPServer, tool Tool[T, R]) error {
|
||||
var v T
|
||||
opts, err := SchemaToOptions(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts = append(opts, mcp.WithDescription(tool.Description()))
|
||||
t := mcp.NewTool(tool.Name(),
|
||||
opts...,
|
||||
)
|
||||
|
||||
s.server.AddTool(t, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
result, err := handleToolCall(ctx, request, tool)
|
||||
if err != nil {
|
||||
logger.With("error", err).Error("handle tool call failed")
|
||||
if wrapped, ok := err.(*errors.Error); ok {
|
||||
return nil, wrapped.Unwrap()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
169
mcp_server/pkg/mcp/schema.go
Normal file
169
mcp_server/pkg/mcp/schema.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// SchemaToOptions Convert struct to MCP ToolOption list
|
||||
func SchemaToOptions(schema any) ([]mcp.ToolOption, error) {
|
||||
t := reflect.TypeOf(schema)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
var options []mcp.ToolOption
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
jsonTag := field.Tag.Get("json")
|
||||
if jsonTag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
desc := field.Tag.Get("desc")
|
||||
required := field.Tag.Get("required") == "true"
|
||||
enumTag := field.Tag.Get("enum")
|
||||
defaultTag := field.Tag.Get("default")
|
||||
minTag := field.Tag.Get("min")
|
||||
maxTag := field.Tag.Get("max")
|
||||
opts := []mcp.PropertyOption{}
|
||||
|
||||
if desc != "" {
|
||||
opts = append(opts, mcp.Description(desc))
|
||||
}
|
||||
if required {
|
||||
opts = append(opts, mcp.Required())
|
||||
}
|
||||
if enumTag != "" && field.Type.Kind() == reflect.String {
|
||||
enumValues := strings.Split(enumTag, ",")
|
||||
opts = append(opts, mcp.Enum(enumValues...))
|
||||
}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
|
||||
if defaultTag != "" {
|
||||
if defaultValue, err := strconv.Atoi(defaultTag); err == nil {
|
||||
opts = append(opts, mcp.DefaultNumber(float64(defaultValue)))
|
||||
}
|
||||
}
|
||||
if minTag != "" {
|
||||
if minValue, err := strconv.Atoi(minTag); err == nil {
|
||||
opts = append(opts, mcp.Min(float64(minValue)))
|
||||
}
|
||||
}
|
||||
if maxTag != "" {
|
||||
if maxValue, err := strconv.Atoi(maxTag); err == nil {
|
||||
opts = append(opts, mcp.Max(float64(maxValue)))
|
||||
}
|
||||
}
|
||||
options = append(options, mcp.WithNumber(jsonTag, opts...))
|
||||
case reflect.Bool:
|
||||
if defaultTag != "" {
|
||||
if defaultValue, err := strconv.ParseBool(defaultTag); err == nil {
|
||||
opts = append(opts, mcp.DefaultBool(defaultValue))
|
||||
}
|
||||
}
|
||||
options = append(options, mcp.WithBoolean(jsonTag, opts...))
|
||||
case reflect.String:
|
||||
if defaultTag != "" {
|
||||
opts = append(opts, mcp.DefaultString(defaultTag))
|
||||
}
|
||||
options = append(options, mcp.WithString(jsonTag, opts...))
|
||||
case reflect.Struct:
|
||||
subSchema := reflect.New(field.Type).Interface()
|
||||
subOptions, err := SchemaToOptions(subSchema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a temporary Tool to get JSON Schema of sub-struct
|
||||
tempTool := mcp.NewTool("temp", subOptions...)
|
||||
tempJSON, _ := tempTool.MarshalJSON()
|
||||
var tempMap map[string]any
|
||||
if err := json.Unmarshal(tempJSON, &tempMap); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract properties from temporary Tool
|
||||
if inputSchema, ok := tempMap["inputSchema"].(map[string]any); ok {
|
||||
if properties, ok := inputSchema["properties"].(map[string]any); ok {
|
||||
// Check if there are required fields
|
||||
if required, ok := inputSchema["required"].([]any); ok {
|
||||
// Add required field information to corresponding properties
|
||||
for _, req := range required {
|
||||
if reqStr, ok := req.(string); ok {
|
||||
if prop, ok := properties[reqStr].(map[string]any); ok {
|
||||
prop["required"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opts = append(opts, mcp.Properties(properties))
|
||||
}
|
||||
}
|
||||
options = append(options, mcp.WithObject(jsonTag, opts...))
|
||||
|
||||
case reflect.Slice:
|
||||
elemType := field.Type.Elem()
|
||||
var items map[string]any
|
||||
|
||||
switch elemType.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
|
||||
items = map[string]any{
|
||||
"type": "number",
|
||||
}
|
||||
case reflect.Bool:
|
||||
items = map[string]any{
|
||||
"type": "boolean",
|
||||
}
|
||||
case reflect.String:
|
||||
items = map[string]any{
|
||||
"type": "string",
|
||||
}
|
||||
case reflect.Struct:
|
||||
subSchema := reflect.New(elemType).Interface()
|
||||
subOptions, err := SchemaToOptions(subSchema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a temporary Tool to get JSON Schema of sub-struct
|
||||
tempTool := mcp.NewTool("temp", subOptions...)
|
||||
tempJSON, _ := tempTool.MarshalJSON()
|
||||
var tempMap map[string]any
|
||||
if err := json.Unmarshal(tempJSON, &tempMap); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract properties from temporary Tool
|
||||
if inputSchema, ok := tempMap["inputSchema"].(map[string]any); ok {
|
||||
if properties, ok := inputSchema["properties"].(map[string]any); ok {
|
||||
// Check if there are required fields
|
||||
if required, ok := inputSchema["required"].([]any); ok {
|
||||
// Add required field information to corresponding properties
|
||||
for _, req := range required {
|
||||
if reqStr, ok := req.(string); ok {
|
||||
if prop, ok := properties[reqStr].(map[string]any); ok {
|
||||
prop["required"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
items = map[string]any{
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opts = append(opts, mcp.Items(items))
|
||||
options = append(options, mcp.WithArray(jsonTag, opts...))
|
||||
}
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
227
mcp_server/pkg/mcp/schema_test.go
Normal file
227
mcp_server/pkg/mcp/schema_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func TestSchemaToOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args any
|
||||
want mcp.Tool
|
||||
}{
|
||||
{
|
||||
name: "test number",
|
||||
args: struct {
|
||||
A int `json:"a" desc:"number a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number int64",
|
||||
args: struct {
|
||||
A int64 `json:"a" desc:"number a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number int64",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number float64",
|
||||
args: struct {
|
||||
A float64 `json:"a" desc:"number a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number float64",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number default",
|
||||
args: struct {
|
||||
A int `json:"a" desc:"number a" required:"true" default:"10"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number default",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a"), mcp.DefaultNumber(10)),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number min max",
|
||||
args: struct {
|
||||
A int `json:"a" desc:"number a" required:"true" min:"10" max:"20"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number min max",
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("number a"), mcp.Min(10), mcp.Max(20)),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test number optional",
|
||||
args: struct {
|
||||
A int `json:"a" desc:"number a"`
|
||||
}{},
|
||||
want: mcp.NewTool("test number optional",
|
||||
mcp.WithNumber("a", mcp.Description("number a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test boolean",
|
||||
args: struct {
|
||||
A bool `json:"a" desc:"boolean a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test boolean",
|
||||
mcp.WithBoolean("a", mcp.Required(), mcp.Description("boolean a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test string",
|
||||
args: struct {
|
||||
A string `json:"a" desc:"string a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test string",
|
||||
mcp.WithString("a", mcp.Required(), mcp.Description("string a")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test string default",
|
||||
args: struct {
|
||||
A string `json:"a" desc:"string a" required:"true" default:"hello"`
|
||||
}{},
|
||||
want: mcp.NewTool("test string default",
|
||||
mcp.WithString("a", mcp.Required(), mcp.Description("string a"), mcp.DefaultString("hello")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test string enum",
|
||||
args: struct {
|
||||
A string `json:"a" desc:"string a" required:"true" enum:"1,2,3"`
|
||||
}{},
|
||||
want: mcp.NewTool("test string enum",
|
||||
mcp.WithString("a", mcp.Required(), mcp.Description("string a"), mcp.Enum("1", "2", "3")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test object",
|
||||
args: struct {
|
||||
A struct {
|
||||
B int `json:"b" desc:"number b" required:"true"`
|
||||
} `json:"a" desc:"object a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test object",
|
||||
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
|
||||
mcp.Properties(map[string]any{
|
||||
"b": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number b",
|
||||
"required": true,
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test object optional",
|
||||
args: struct {
|
||||
A struct {
|
||||
B int `json:"b" desc:"number b" required:"true"`
|
||||
C int `json:"c" desc:"number c"`
|
||||
} `json:"a" desc:"object a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test object optional",
|
||||
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
|
||||
mcp.Properties(map[string]any{
|
||||
"b": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number b",
|
||||
"required": true,
|
||||
},
|
||||
"c": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number c",
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test nested object",
|
||||
args: struct {
|
||||
A struct {
|
||||
B struct {
|
||||
C int `json:"c" desc:"number c" required:"true"`
|
||||
} `json:"b" desc:"object b" required:"true"`
|
||||
} `json:"a" desc:"object a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test nested object",
|
||||
mcp.WithObject("a", mcp.Required(), mcp.Description("object a"),
|
||||
mcp.Properties(map[string]any{
|
||||
"b": map[string]any{
|
||||
"type": "object",
|
||||
"description": "object b",
|
||||
"required": true,
|
||||
"properties": map[string]any{
|
||||
"c": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number c",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test array",
|
||||
args: struct {
|
||||
A []int `json:"a" desc:"array a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test array",
|
||||
mcp.WithArray("a", mcp.Required(), mcp.Description("array a"),
|
||||
mcp.Items(map[string]any{
|
||||
"type": "number",
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test array of object",
|
||||
args: struct {
|
||||
A []struct {
|
||||
B int `json:"b" desc:"number b" required:"true"`
|
||||
} `json:"a" desc:"array of object a" required:"true"`
|
||||
}{},
|
||||
want: mcp.NewTool("test array of object",
|
||||
mcp.WithArray("a", mcp.Required(), mcp.Description("array of object a"),
|
||||
mcp.Items(map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"b": map[string]any{
|
||||
"type": "number",
|
||||
"description": "number b",
|
||||
"required": true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := SchemaToOptions(tt.args)
|
||||
if err != nil {
|
||||
t.Errorf("SchemaToOptions() error = %v", err)
|
||||
return
|
||||
}
|
||||
s1, _ := mcp.NewTool(tt.name, got...).MarshalJSON()
|
||||
s2, _ := tt.want.MarshalJSON()
|
||||
if !reflect.DeepEqual(s1, s2) {
|
||||
t.Errorf("\n got %v\n want %v", string(s1), string(s2))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
5
version.json
Normal file
5
version.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"latest_version": "v9.2.7",
|
||||
"rec_version": "v9.2.7",
|
||||
"lts_version": "v9.1.0-lts"
|
||||
}
|
||||
Reference in New Issue
Block a user