feat: mcp go

This commit is contained in:
xbingW
2025-04-10 17:30:17 +08:00
parent 84c2db9ee3
commit 681e7f95f2
49 changed files with 262 additions and 1189 deletions

View File

@@ -1,28 +0,0 @@
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"]

View File

@@ -1,253 +0,0 @@
# 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](https://img.shields.io/badge/Docker-Supported-2496ED?style=flat-square&logo=docker&logoColor=white)](docker-compose.yml)
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat-square&logo=go&logoColor=white)](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
```
## Tools
### Application Management
- **create_application**
### Rule Management
- **create_blacklist_rule**
- **create_whitelist_rule**
For more API details, please refer to the [API Documentation](https://master.safeline-ce.staging.dev.in.chaitin.net:9443/swagger/index.html#).
## 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`

View File

@@ -1 +0,0 @@
3.13

View File

@@ -1,11 +1,28 @@
FROM ghcr.io/astral-sh/uv:python3.13-alpine
COPY requirements.txt /app/requirements.txt
FROM golang:1.24-alpine AS builder
WORKDIR /app
RUN uv pip sync requirements.txt --system -i https://pypi.tuna.tsinghua.edu.cn/simple
RUN apk add --no-cache git
COPY . /app
COPY go.mod go.sum ./
CMD ["python", "__main__.py"]
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"]

View File

@@ -1,48 +1,253 @@
## mcp_server - A SafeLine WAF mcp server
# SafeLine MCP Server
- Easy to use
- one command to run mcp_server
- Easy to develop
- add yoor own tools to `tools` dirctory without modify other files
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.
### quick start
[![Docker](https://img.shields.io/badge/Docker-Supported-2496ED?style=flat-square&logo=docker&logoColor=white)](docker-compose.yml)
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat-square&logo=go&logoColor=white)](go.mod)
```shell
docker compose -f docker-compose.yaml up -d
## 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
```
### custom your own tool
#### Method 2: Using docker-compose
#### Hello Tool Example
```bash
# 1. Clone repository
git clone https://github.com/chaitin/safeline-mcp.git
cd safeline-mcp
This tool used to say hello to someone
# 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
1. create file `tools/hello.py`
# 3. Start service
docker compose -f docker-compose.yml up -d
```
```python
from pydantic import BaseModel, Field
from tools import Tool, ABCTool, tools
#### Method 3: Using Go
# register to global tools
@tools.register
# Hello describe function paramters
class Hello(BaseModel, ABCTool):
# tools paramters
name: str = Field(description="username to say hello")
```bash
# 1. Clone repository
git clone https://github.com/chaitin/SafeLine.git
cd safeline-mcp
# run is tool logic, must use classmethod
@classmethod
async def run(arguments: dict) -> str:
req = Hello.model_validate(arguments)
return f"Hello {req.name}"
# 2. Install dependencies
go mod download
# tool description, must use classmethod
@classmethod
def tool(self) -> Tool:
return Tool(
name="hello",
description="say hello to someone",
inputSchema=self.model_json_schema()
)
# 3. Configure config.yaml
cp config.yaml.example config.yaml
# Edit config.yaml with necessary configurations
# 4. Run service
go run main.go
```
## Tools
### Application Management
- **create_application**
### Rule Management
- **create_blacklist_rule**
- **create_whitelist_rule**
For more API details, please refer to the [API Documentation](https://master.safeline-ce.staging.dev.in.chaitin.net:9443/swagger/index.html#).
## 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`

View File

@@ -1,3 +0,0 @@
from .config import Config
GLOBAL_CONFIG = Config.from_env()

View File

@@ -1,4 +0,0 @@
from server import main
if __name__ == "__main__":
main()

View File

@@ -1,3 +0,0 @@
from .config import Config
GLOBAL_CONFIG = Config.from_env()

View File

@@ -1,56 +0,0 @@
import os
import logging
class Config:
SAFELINE_ADDRESS: str
SAFELINE_API_TOKEN: str
SECRET: str
LISTEN_PORT: int
LISTEN_ADDRESS: str
DEBUG: bool
def __init__(self):
set_log_level()
if os.getenv("MCP_SERVER_DEBUG"):
self.DEBUG = True
else:
self.DEBUG = False
self.SAFELINE_ADDRESS = os.getenv("SAFELINE_ADDRESS")
if self.SAFELINE_ADDRESS:
self.SAFELINE_ADDRESS = self.SAFELINE_ADDRESS.removesuffix("/")
self.SAFELINE_API_TOKEN = os.getenv("SAFELINE_API_TOKEN")
self.SECRET = os.getenv("SAFELINE_SECRET")
env_listen_port = os.getenv("LISTEN_PORT")
if env_listen_port and env_listen_port.isdigit():
self.LISTEN_PORT = int(env_listen_port)
else:
self.LISTEN_PORT = 5678
env_listen_address = os.getenv("LISTEN_ADDRESS")
if env_listen_address:
self.LISTEN_ADDRESS = env_listen_address
else:
self.LISTEN_ADDRESS = "0.0.0.0"
@staticmethod
def from_env():
return Config()
def set_log_level():
level = logging.WARN
log_level = os.getenv("MCO_SERVER_LOG_LEVEL")
if log_level:
match log_level.lower():
case "debug":
level = logging.DEBUG
case "info":
level = logging.INFO
case "warn":
level = logging.WARN
case "error":
level = logging.ERROR
case "critical":
level = logging.CRITICAL
logging.basicConfig(level=level, format="%(asctime)s - %(levelname)s - %(message)s")

View File

@@ -1,12 +0,0 @@
services:
mcp_server:
image: chaitin/safeline-mcp:latest
container_name: mcp_server
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

View File

@@ -1,21 +0,0 @@
from starlette.requests import HTTPConnection
from starlette.responses import PlainTextResponse
from starlette.types import ASGIApp, Receive, Scope, Send
from config import GLOBAL_CONFIG
class AuthenticationMiddleware:
def __init__(
self,
app: ASGIApp,
) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
conn = HTTPConnection(scope)
if GLOBAL_CONFIG.SECRET and GLOBAL_CONFIG.SECRET != "" and conn.headers.get("Secret") != GLOBAL_CONFIG.SECRET:
response = PlainTextResponse("Unauthorized", status_code=401)
await response(scope, receive, send)
return
await self.app(scope, receive, send)

View File

@@ -1,9 +0,0 @@
[project]
name = "mcp_server"
version = "0.1.0"
description = "SafeLine WAF mcp server"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"mcp[cli]>=1.6.0",
]

View File

@@ -1,76 +0,0 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
annotated-types==0.7.0
# via pydantic
anyio==4.9.0
# via
# httpx
# mcp
# sse-starlette
# starlette
certifi==2025.1.31
# via
# httpcore
# httpx
click==8.1.8
# via
# typer
# uvicorn
h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.7
# via httpx
httpx==0.28.1
# via mcp
httpx-sse==0.4.0
# via mcp
idna==3.10
# via
# anyio
# httpx
markdown-it-py==3.0.0
# via rich
mcp==1.6.0
# via mcp_server (pyproject.toml)
mdurl==0.1.2
# via markdown-it-py
pydantic==2.11.1
# via
# mcp
# pydantic-settings
pydantic-core==2.33.0
# via pydantic
pydantic-settings==2.8.1
# via mcp
pygments==2.19.1
# via rich
python-dotenv==1.1.0
# via
# mcp
# pydantic-settings
rich==14.0.0
# via typer
shellingham==1.5.4
# via typer
sniffio==1.3.1
# via anyio
sse-starlette==2.2.1
# via mcp
starlette==0.46.1
# via
# mcp
# sse-starlette
typer==0.15.2
# via mcp
typing-extensions==4.13.0
# via
# pydantic
# pydantic-core
# typer
# typing-inspection
typing-inspection==0.4.0
# via pydantic
uvicorn==0.34.0
# via mcp

View File

@@ -1,46 +0,0 @@
from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.sse
from starlette.applications import Starlette
from starlette.routing import Route, Mount
from starlette.requests import Request
import uvicorn
from tools import tools
from config import GLOBAL_CONFIG
from middleware import AuthenticationMiddleware
from starlette.middleware import Middleware
# Create an MCP server
mcp_server = Server("SafeLine WAF mcp server")
sse = mcp.server.sse.SseServerTransport("/messages/")
@mcp_server.list_tools()
async def list_tools() -> list[Tool]:
return tools.all()
@mcp_server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
result = await tools.run(name, arguments)
return [TextContent(
type="text",
text=f"{name} result: {result}"
)]
async def handle_sse(request: Request) -> None:
async with sse.connect_sse(
request.scope, request.receive, request._send
) as [read_stream, write_stream]:
await mcp_server.run(
read_stream, write_stream, mcp_server.create_initialization_options()
)
def main():
starlette_app = Starlette(debug=GLOBAL_CONFIG.DEBUG,routes=[
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=sse.handle_post_message, middleware=[Middleware(AuthenticationMiddleware)]),
])
uvicorn.run(starlette_app, host=GLOBAL_CONFIG.LISTEN_ADDRESS, port=GLOBAL_CONFIG.LISTEN_PORT)

View File

@@ -1,49 +0,0 @@
from mcp.types import Tool
from abc import ABC, abstractmethod
import os
import importlib
import logging
class ABCTool(ABC):
@classmethod
@abstractmethod
async def run(self, arguments:dict) -> str:
pass
@classmethod
@abstractmethod
def tool(self) -> Tool:
pass
class ToolRegister:
_dict: dict[str, ABCTool] = {}
def register(self, tool: ABCTool) -> ABCTool:
tool_name = tool.tool().name
logging.info(f"Registering tool: {tool_name}")
if tool_name in self._dict:
raise ValueError(f"Tool {tool_name} already registered")
self._dict[tool_name] = tool
return tool
def all(self) -> list[Tool]:
return [tool.tool() for tool in self._dict.values()]
async def run(self, name: str, arguments: dict) -> str:
if name not in self._dict:
raise ValueError(f"Unknown tool: {name}")
return await self._dict[name].run(arguments)
def import_all_tools():
for module in os.listdir(os.path.dirname(__file__)):
if module == "__init__.py" or len(module) < 3 or not module.endswith(".py"):
continue
module_name = module[:-3]
importlib.import_module(f".{module_name}", package=__name__)
tools = ToolRegister()
import_all_tools()

View File

@@ -1,43 +0,0 @@
from pydantic import BaseModel, Field
from utils.request import post_slce_api
from tools import Tool, ABCTool, tools
from urllib.parse import urlparse
@tools.register
class CreateHttpApplication(BaseModel, ABCTool):
domain: str = Field(default="",description="application domain, if empty, match all domain")
port: int = Field(min=1, max=65535,description="application listen port, must between 1 and 65535")
upstream: str = Field(description="application proxy address, must be a valid url")
@classmethod
async def run(self, arguments:dict) -> str:
try:
req = CreateHttpApplication.model_validate(arguments)
parsed_upstream = urlparse(req.upstream)
if parsed_upstream.scheme != "https" and parsed_upstream.scheme != "http":
return "invalid upstream scheme"
if parsed_upstream.hostname == "":
return "invalid upstream host"
except Exception as e:
return str(e)
return await post_slce_api("/api/open/site",{
"server_names": [req.domain],
"ports": [ str(req.port) ],
"upstreams": [ req.upstream ],
"type": 0,
"static_default": 1,
"health_check": True,
"load_balance": {
"balance_type": 1
}
})
@classmethod
def tool(self) -> Tool:
return Tool(
name="waf_create_http_application",
description="在雷池 WAF 上创建一个站点应用",
inputSchema=self.model_json_schema()
)

View File

@@ -1,55 +0,0 @@
from pydantic import BaseModel, Field
from utils.request import post_slce_api
from tools import Tool, ABCTool, tools
import ipaddress
@tools.register
class CreateIPCustomRule(BaseModel, ABCTool):
ip: str = Field(description="request ip to allow or block")
action: int = Field(min=0, max=1,description="1: block, 0: allow")
@classmethod
async def run(self, arguments:dict) -> str:
try:
req = CreateIPCustomRule.model_validate(arguments)
ipaddress.ip_address(req.ip)
except Exception as e:
return str(e)
name = ""
match req.action:
case 0:
name += "allow "
case 1:
name += "block "
case _:
return "invalid action"
if not req.ip or req.ip == "":
return "ip is required"
name += f"ip: {req.ip}"
return await post_slce_api("/api/open/policy",{
"name": name,
"is_enabled": True,
"pattern": [
[
{
"k": "src_ip",
"op": "eq",
"v": [req.ip],
"sub_k": ""
},
]
],
"action": req.action
})
@classmethod
def tool(self) -> Tool:
return Tool(
name="waf_create_ip_custom_rule",
description="以 客户端 IP 地址为条件,在雷池 WAF 上创建一个黑/白名单",
inputSchema=self.model_json_schema()
)

View File

@@ -1,53 +0,0 @@
from pydantic import BaseModel, Field
from utils.request import post_slce_api
from tools import Tool, ABCTool, tools
@tools.register
class CreatePathCustomRule(BaseModel, ABCTool):
path: str = Field(description="request path to block or allow")
action: int = Field(min=0, max=1,description="1: block, 0: allow")
@classmethod
async def run(self, arguments:dict) -> str:
try:
req = CreatePathCustomRule.model_validate(arguments)
except Exception as e:
return str(e)
name = ""
match req.action:
case 0:
name += "allow "
case 1:
name += "block "
case _:
return "invalid action"
if not req.path or req.path == "":
return "path is required"
name += f"path: {req.path}"
return await post_slce_api("/api/open/policy",{
"name": name,
"is_enabled": True,
"pattern": [
[
{
"k": "uri_no_query",
"op": "eq",
"v": [req.path],
"sub_k": ""
},
]
],
"action": req.action
})
@classmethod
def tool(self) -> Tool:
return Tool(
name="waf_create_path_custom_rule",
description="以 URL Path 为条件,在雷池 WAF 上创建一个黑/白名单",
inputSchema=self.model_json_schema()
)

View File

@@ -1,29 +0,0 @@
from pydantic import BaseModel, Field
from utils.request import get_slce_api
from tools import Tool, ABCTool, tools
@tools.register
class GetAttackEvents(BaseModel, ABCTool):
ip: str = Field(default="", description="the attacker's client IP address")
size: int = Field(default=10, min=1, max=100, description="the number of results to return")
start: str = Field(default="", description="start time, millisecond timestamp")
end: str = Field(default="", description="end time, millisecond timestamp")
@classmethod
async def run(self, arguments:dict) -> str:
try:
req = GetAttackEvents.model_validate(arguments)
if req.size < 1 or req.size > 100:
return "size must be between 1 and 100"
except Exception as e:
return str(e)
return await get_slce_api(f"api/open/events?page=1&page_size={req.size}&ip={req.ip}&start={req.start}&end={req.end}")
@classmethod
def tool(self) -> Tool:
return Tool(
name="waf_get_attack_events",
description="获取雷池 WAF 所记录的攻击事件",
inputSchema=self.model_json_schema()
)

View File

@@ -1,62 +0,0 @@
from httpx import AsyncClient
from config import GLOBAL_CONFIG
import httpx
import json
def get_response_data(response: httpx.Response) -> dict:
if response.status_code != 200:
raise Exception(f"response status code: {response.status_code}")
data = response.json()
if data["msg"] is not None and data["msg"] != "":
raise Exception(f"request SafeLine API failed: {data['msg']}")
if data["err"] is not None and data["err"] != "":
raise Exception(f"request SafeLine API failed: {data['err']}")
return data['data']
def check_slce_response(response: httpx.Response) -> str:
try:
get_response_data(response)
except Exception as e:
return str(e)
return "success"
def check_slce_get_response(response: httpx.Response) -> str:
try:
data = get_response_data(response)
if data:
return json.dumps(data)
return "empty response data"
except Exception as e:
return str(e)
async def get_slce_api(path: str) -> str:
if not path.startswith("/"):
path = f"/{path}"
try:
async with AsyncClient(verify=False) as client:
response = await client.get(f"{GLOBAL_CONFIG.SAFELINE_ADDRESS}{path}", headers={
"X-SLCE-API-TOKEN": f"{GLOBAL_CONFIG.SAFELINE_API_TOKEN}"
})
return check_slce_get_response(response)
except Exception as e:
return str(e)
async def post_slce_api(path: str, req_body: dict) -> str:
if not path.startswith("/"):
path = f"/{path}"
try:
async with AsyncClient(verify=False) as client:
response = await client.post(f"{GLOBAL_CONFIG.SAFELINE_ADDRESS}{path}", json=req_body, headers={
"X-SLCE-API-TOKEN": f"{GLOBAL_CONFIG.SAFELINE_API_TOKEN}"
})
return check_slce_response(response)
except Exception as e:
return str(e)

346
mcp_server/uv.lock generated
View File

@@ -1,346 +0,0 @@
version = 1
revision = 1
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "httpx-sse"
version = "0.4.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
]
[[package]]
name = "mcp"
version = "1.6.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "uvicorn" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 },
]
[package.optional-dependencies]
cli = [
{ name = "python-dotenv" },
{ name = "typer" },
]
[[package]]
name = "mcp-server"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "mcp", extra = ["cli"] },
]
[package.metadata]
requires-dist = [{ name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "pydantic"
version = "2.11.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 },
]
[[package]]
name = "pydantic-core"
version = "2.33.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 },
]
[[package]]
name = "pydantic-settings"
version = "2.8.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "python-dotenv"
version = "1.1.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
]
[[package]]
name = "rich"
version = "14.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "sse-starlette"
version = "2.2.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
]
[[package]]
name = "starlette"
version = "0.46.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
]
[[package]]
name = "typer"
version = "0.15.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
]
[[package]]
name = "typing-extensions"
version = "4.13.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 },
]
[[package]]
name = "typing-inspection"
version = "0.4.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
]
[[package]]
name = "uvicorn"
version = "0.34.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
]