Initial commit

This commit is contained in:
2026-04-07 09:03:48 +08:00
commit c9ffb52b7f
713 changed files with 111641 additions and 0 deletions

159
server/mcp/api_creator.go Normal file
View File

@@ -0,0 +1,159 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/mark3labs/mcp-go/mcp"
)
func init() {
RegisterTool(&ApiCreator{})
}
type ApiCreateRequest struct {
Path string `json:"path"`
Description string `json:"description"`
ApiGroup string `json:"apiGroup"`
Method string `json:"method"`
}
type ApiCreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
ApiID uint `json:"apiId"`
Path string `json:"path"`
Method string `json:"method"`
}
type ApiCreator struct{}
func (a *ApiCreator) New() mcp.Tool {
return mcp.NewTool("create_api",
mcp.WithDescription(`创建后端API记录用于AI编辑器自动添加API接口时自动创建对应的API权限记录。
**重要限制:**
- 当使用gva_auto_generate工具且needCreatedModules=true时模块创建会自动生成API权限不应调用此工具
- 仅在以下情况使用1) 单独创建API不涉及模块创建2) AI编辑器自动添加API3) router下的文件产生路径变化时`),
mcp.WithString("path",
mcp.Required(),
mcp.Description("API路径/user/create"),
),
mcp.WithString("description",
mcp.Required(),
mcp.Description("API中文描述创建用户"),
),
mcp.WithString("apiGroup",
mcp.Required(),
mcp.Description("API组名称用于分类管理用户管理"),
),
mcp.WithString("method",
mcp.Description("HTTP方法"),
mcp.DefaultString("POST"),
),
mcp.WithString("apis",
mcp.Description("批量创建API的JSON字符串格式[{\"path\":\"/user/create\",\"description\":\"创建用户\",\"apiGroup\":\"用户管理\",\"method\":\"POST\"}]"),
),
)
}
func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
var apis []ApiCreateRequest
if apisStr, ok := args["apis"].(string); ok && apisStr != "" {
if err := json.Unmarshal([]byte(apisStr), &apis); err != nil {
return nil, fmt.Errorf("apis 参数格式错误: %w", err)
}
} else {
path, ok := args["path"].(string)
if !ok || path == "" {
return nil, errors.New("path 参数是必需的")
}
description, ok := args["description"].(string)
if !ok || description == "" {
return nil, errors.New("description 参数是必需的")
}
apiGroup, ok := args["apiGroup"].(string)
if !ok || apiGroup == "" {
return nil, errors.New("apiGroup 参数是必需的")
}
method := "POST"
if value, ok := args["method"].(string); ok && value != "" {
method = value
}
apis = append(apis, ApiCreateRequest{
Path: path,
Description: description,
ApiGroup: apiGroup,
Method: method,
})
}
if len(apis) == 0 {
return nil, errors.New("没有要创建的API")
}
responses := make([]ApiCreateResponse, 0, len(apis))
successCount := 0
for _, apiReq := range apis {
_, err := postUpstream[map[string]any](ctx, "/api/createApi", system.SysApi{
Path: apiReq.Path,
Description: apiReq.Description,
ApiGroup: apiReq.ApiGroup,
Method: apiReq.Method,
})
if err != nil {
responses = append(responses, ApiCreateResponse{
Success: false,
Message: fmt.Sprintf("创建API失败: %v", err),
Path: apiReq.Path,
Method: apiReq.Method,
})
continue
}
lookupResp, lookupErr := postUpstream[pageResultData[[]system.SysApi]](ctx, "/api/getApiList", systemReq.SearchApiParams{
SysApi: system.SysApi{
Path: apiReq.Path,
Method: apiReq.Method,
},
PageInfo: commonReq.PageInfo{
Page: 1,
PageSize: 1,
},
})
var apiID uint
if lookupErr == nil && len(lookupResp.Data.List) > 0 {
apiID = lookupResp.Data.List[0].ID
}
responses = append(responses, ApiCreateResponse{
Success: true,
Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path),
ApiID: apiID,
Path: apiReq.Path,
Method: apiReq.Method,
})
successCount++
}
result := map[string]any{
"success": successCount > 0,
"totalCount": len(apis),
"successCount": successCount,
"failedCount": len(apis) - successCount,
"details": responses,
}
return textResultWithJSON("API创建结果", result)
}

95
server/mcp/api_lister.go Normal file
View File

@@ -0,0 +1,95 @@
package mcpTool
import (
"context"
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
"github.com/gin-gonic/gin"
"github.com/mark3labs/mcp-go/mcp"
)
func init() {
RegisterTool(&ApiLister{})
}
type ApiInfo struct {
ID uint `json:"id,omitempty"`
Path string `json:"path"`
Description string `json:"description,omitempty"`
ApiGroup string `json:"apiGroup,omitempty"`
Method string `json:"method"`
Source string `json:"source"`
}
type ApiListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
DatabaseApis []ApiInfo `json:"databaseApis"`
GinApis []ApiInfo `json:"ginApis"`
TotalCount int `json:"totalCount"`
}
type mcpRoutesResponse struct {
Routes gin.RoutesInfo `json:"routes"`
}
type ApiLister struct{}
func (a *ApiLister) New() mcp.Tool {
return mcp.NewTool("list_all_apis",
mcp.WithDescription(`获取系统中所有的API接口分为两组
**功能说明:**
- 返回数据库中已注册的API列表
- 返回gin框架中实际注册的路由API列表
- 帮助前端判断是使用现有API还是需要创建新的API,如果api在前端未使用且需要前端调用的时候请到api文件夹下对应模块的js中添加方法并暴露给当前业务调用
**返回数据结构:**
- databaseApis: 数据库中的API记录包含ID、描述、分组等完整信息
- ginApis: gin路由中的API仅包含路径和方法需要AI根据路径自行揣摩路径的业务含义例如/api/user/:id 表示根据用户ID获取用户信息`),
mcp.WithString("_placeholder",
mcp.Description("占位符防止json schema校验失败"),
),
)
}
func (a *ApiLister) Handle(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
apiResp, err := postUpstream[systemRes.SysAPIListResponse](ctx, "/api/getAllApis", map[string]any{})
if err != nil {
return nil, err
}
routeResp, err := postUpstream[mcpRoutesResponse](ctx, "/autoCode/mcpRoutes", map[string]any{})
if err != nil {
return nil, err
}
databaseApis := make([]ApiInfo, 0, len(apiResp.Data.Apis))
for _, api := range apiResp.Data.Apis {
databaseApis = append(databaseApis, ApiInfo{
ID: api.ID,
Path: api.Path,
Description: api.Description,
ApiGroup: api.ApiGroup,
Method: api.Method,
Source: "database",
})
}
ginApis := make([]ApiInfo, 0, len(routeResp.Data.Routes))
for _, route := range routeResp.Data.Routes {
ginApis = append(ginApis, ApiInfo{
Path: route.Path,
Method: route.Method,
Source: "gin",
})
}
return textResultWithJSON("", ApiListResponse{
Success: true,
Message: "获取API列表成功",
DatabaseApis: databaseApis,
GinApis: ginApis,
TotalCount: len(databaseApis) + len(ginApis),
})
}

View File

@@ -0,0 +1,48 @@
package mcpTool
import (
"context"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
)
func fetchAutoCodePackages(ctx context.Context) ([]model.SysAutoCodePackage, error) {
resp, err := postUpstream[map[string][]model.SysAutoCodePackage](ctx, "/autoCode/getPackage", map[string]any{})
if err != nil {
return nil, err
}
return resp.Data["pkgs"], nil
}
func fetchAutoCodeHistories(ctx context.Context) ([]model.SysAutoCodeHistory, error) {
resp, err := postUpstream[pageResultData[[]model.SysAutoCodeHistory]](ctx, "/autoCode/getSysHistory", commonReq.PageInfo{
Page: 1,
PageSize: 10000,
})
if err != nil {
return nil, err
}
return resp.Data.List, nil
}
func createAutoCodePackage(ctx context.Context, info *systemReq.SysAutoCodePackageCreate) error {
_, err := postUpstream[map[string]any](ctx, "/autoCode/createPackage", info)
return err
}
func createAutoCodeModule(ctx context.Context, info systemReq.AutoCode) error {
_, err := postUpstream[map[string]any](ctx, "/autoCode/createTemp", info)
return err
}
func deleteAutoCodePackage(ctx context.Context, id uint) error {
_, err := postUpstream[map[string]any](ctx, "/autoCode/delPackage", commonReq.GetById{ID: int(id)})
return err
}
func deleteAutoCodeHistory(ctx context.Context, id uint) error {
_, err := postUpstream[map[string]any](ctx, "/autoCode/delSysHistory", commonReq.GetById{ID: int(id)})
return err
}

View File

@@ -0,0 +1,44 @@
package client
import (
"context"
"errors"
mcpClient "github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
)
func NewClient(baseURL, name, version, serverName string, headers ...map[string]string) (*mcpClient.Client, error) {
options := make([]transport.StreamableHTTPCOption, 0, 1)
if len(headers) > 0 && len(headers[0]) > 0 {
options = append(options, transport.WithHTTPHeaders(headers[0]))
}
client, err := mcpClient.NewStreamableHttpClient(baseURL, options...)
if err != nil {
return nil, err
}
ctx := context.Background()
if err := client.Start(ctx); err != nil {
return nil, err
}
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: name,
Version: version,
}
result, err := client.Initialize(ctx, initRequest)
if err != nil {
return nil, err
}
if result.ServerInfo.Name != serverName {
return nil, errors.New("server name mismatch")
}
return client, nil
}

View File

@@ -0,0 +1,132 @@
package client
import (
"context"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"testing"
)
// 测试 MCP 客户端连接
func TestMcpClientConnection(t *testing.T) {
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
defer c.Close()
if err != nil {
t.Fatalf(err.Error())
}
}
func TestTools(t *testing.T) {
t.Run("currentTime", func(t *testing.T) {
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
defer c.Close()
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
ctx := context.Background()
request := mcp.CallToolRequest{}
request.Params.Name = "currentTime"
request.Params.Arguments = map[string]interface{}{
"timezone": "UTC+8",
}
result, err := c.CallTool(ctx, request)
if err != nil {
t.Fatalf("方法调用错误: %v", err)
}
if len(result.Content) != 1 {
t.Errorf("应该有且仅返回1条信息但是现在有 %d", len(result.Content))
}
if content, ok := result.Content[0].(mcp.TextContent); ok {
t.Logf("成功返回信息%s", content.Text)
} else {
t.Logf("返回为止类型信息%+v", content)
}
})
t.Run("getNickname", func(t *testing.T) {
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
defer c.Close()
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
ctx := context.Background()
// Initialize
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "test-client",
Version: "1.0.0",
}
_, err = c.Initialize(ctx, initRequest)
if err != nil {
t.Fatalf("初始化失败: %v", err)
}
request := mcp.CallToolRequest{}
request.Params.Name = "getNickname"
request.Params.Arguments = map[string]interface{}{
"username": "admin",
}
result, err := c.CallTool(ctx, request)
if err != nil {
t.Fatalf("方法调用错误: %v", err)
}
if len(result.Content) != 1 {
t.Errorf("应该有且仅返回1条信息但是现在有 %d", len(result.Content))
}
if content, ok := result.Content[0].(mcp.TextContent); ok {
t.Logf("成功返回信息%s", content.Text)
} else {
t.Logf("返回为止类型信息%+v", content)
}
})
}
func TestGetTools(t *testing.T) {
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
defer c.Close()
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
ctx := context.Background()
toolsRequest := mcp.ListToolsRequest{}
toolListResult, err := c.ListTools(ctx, toolsRequest)
if err != nil {
t.Fatalf("获取工具列表失败: %v", err)
}
for i := range toolListResult.Tools {
tool := toolListResult.Tools[i]
fmt.Printf("工具名称: %s\n", tool.Name)
fmt.Printf("工具描述: %s\n", tool.Description)
// 打印参数信息
if tool.InputSchema.Properties != nil {
fmt.Println("参数列表:")
for paramName, prop := range tool.InputSchema.Properties {
required := "否"
// 检查参数是否在必填列表中
for _, reqField := range tool.InputSchema.Required {
if reqField == paramName {
required = "是"
break
}
}
fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n",
paramName, prop.(map[string]any)["type"], prop.(map[string]any)["description"], required)
}
} else {
fmt.Println("该工具没有参数")
}
fmt.Println("-------------------")
}
}

66
server/mcp/context.go Normal file
View File

@@ -0,0 +1,66 @@
package mcpTool
import (
"context"
"net/http"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
type mcpContextKey string
const authTokenContextKey mcpContextKey = "mcp-auth-token"
func WithHTTPRequestContext(ctx context.Context, r *http.Request) context.Context {
token := extractIncomingAuthToken(r.Header)
return context.WithValue(ctx, authTokenContextKey, token)
}
func configuredAuthHeader() string {
if header := strings.TrimSpace(global.GVA_CONFIG.MCP.AuthHeader); header != "" {
return header
}
return "x-token"
}
func ConfiguredAuthHeader() string {
return configuredAuthHeader()
}
func authTokenFromContext(ctx context.Context) string {
token, _ := ctx.Value(authTokenContextKey).(string)
return strings.TrimSpace(token)
}
func extractIncomingAuthToken(headers http.Header) string {
candidates := []string{
configuredAuthHeader(),
"x-token",
"token",
"authorization",
}
seen := make(map[string]struct{}, len(candidates))
for _, name := range candidates {
key := strings.ToLower(strings.TrimSpace(name))
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
value := strings.TrimSpace(headers.Get(name))
if value == "" {
continue
}
if key == "authorization" {
return strings.TrimSpace(strings.TrimPrefix(value, "Bearer "))
}
return value
}
return ""
}

View File

@@ -0,0 +1,174 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/mark3labs/mcp-go/mcp"
)
func init() {
RegisterTool(&DictionaryOptionsGenerator{})
}
type DictionaryOptionsGenerator struct{}
type DictionaryOption struct {
Label string `json:"label"`
Value string `json:"value"`
Sort int `json:"sort"`
}
type DictionaryGenerateRequest struct {
DictType string `json:"dictType"`
FieldDesc string `json:"fieldDesc"`
Options []DictionaryOption `json:"options"`
DictName string `json:"dictName"`
Description string `json:"description"`
}
type DictionaryGenerateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
DictType string `json:"dictType"`
OptionsCount int `json:"optionsCount"`
}
func (d *DictionaryOptionsGenerator) New() mcp.Tool {
return mcp.NewTool("generate_dictionary_options",
mcp.WithDescription("智能生成字典选项并自动创建字典和字典详情"),
mcp.WithString("dictType",
mcp.Required(),
mcp.Description("字典类型,用于标识字典的唯一性"),
),
mcp.WithString("fieldDesc",
mcp.Required(),
mcp.Description("字段描述用于AI理解字段含义"),
),
mcp.WithString("options",
mcp.Required(),
mcp.Description("字典选项JSON字符串格式[{\"label\":\"显示名\",\"value\":\"值\",\"sort\":1}]"),
),
mcp.WithString("dictName",
mcp.Description("字典名称,如果不提供将自动生成"),
),
mcp.WithString("description",
mcp.Description("字典描述"),
),
)
}
func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
dictType, ok := args["dictType"].(string)
if !ok || dictType == "" {
return nil, errors.New("dictType 参数是必需的")
}
fieldDesc, ok := args["fieldDesc"].(string)
if !ok || fieldDesc == "" {
return nil, errors.New("fieldDesc 参数是必需的")
}
optionsStr, ok := args["options"].(string)
if !ok || optionsStr == "" {
return nil, errors.New("options 参数是必需的")
}
var options []DictionaryOption
if err := json.Unmarshal([]byte(optionsStr), &options); err != nil {
return nil, fmt.Errorf("options 参数格式错误: %v", err)
}
if len(options) == 0 {
return nil, errors.New("options 不能为空")
}
req := &DictionaryGenerateRequest{
DictType: dictType,
FieldDesc: fieldDesc,
Options: options,
DictName: stringValue(args["dictName"]),
Description: stringValue(args["description"]),
}
result, err := d.createDictionaryWithOptions(ctx, req)
if err != nil {
return nil, err
}
return textResultWithJSON("字典选项生成结果:", result)
}
func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Context, req *DictionaryGenerateRequest) (*DictionaryGenerateResponse, error) {
existingDict, err := findDictionaryByType(ctx, req.DictType)
if err != nil {
return nil, fmt.Errorf("检查字典是否存在失败: %v", err)
}
if existingDict != nil {
return &DictionaryGenerateResponse{
Success: false,
Message: fmt.Sprintf("字典 %s 已存在,跳过创建", req.DictType),
DictType: req.DictType,
OptionsCount: 0,
}, nil
}
dictName := req.DictName
if dictName == "" {
dictName = d.generateDictionaryName(req.DictType, req.FieldDesc)
}
if err := createDictionary(ctx, system.SysDictionary{
Name: dictName,
Type: req.DictType,
Status: enabledBoolPointer(),
Desc: req.Description,
}); err != nil {
return nil, fmt.Errorf("创建字典失败: %v", err)
}
createdDict, err := findDictionaryByType(ctx, req.DictType)
if err != nil {
return nil, fmt.Errorf("获取创建的字典失败: %v", err)
}
if createdDict == nil {
return nil, fmt.Errorf("获取创建的字典失败")
}
successCount := 0
for _, option := range req.Options {
err := createDictionaryDetail(ctx, system.SysDictionaryDetail{
Label: option.Label,
Value: option.Value,
Status: enabledBoolPointer(),
Sort: option.Sort,
SysDictionaryID: int(createdDict.ID),
})
if err == nil {
successCount++
}
}
return &DictionaryGenerateResponse{
Success: true,
Message: fmt.Sprintf("成功创建字典 %s包含 %d 个选项", req.DictType, successCount),
DictType: req.DictType,
OptionsCount: successCount,
}, nil
}
func (d *DictionaryOptionsGenerator) generateDictionaryName(dictType, fieldDesc string) string {
if fieldDesc != "" {
return fmt.Sprintf("%s字典", fieldDesc)
}
return fmt.Sprintf("%s字典", dictType)
}
func stringValue(value any) string {
if str, ok := value.(string); ok {
return str
}
return ""
}

View File

@@ -0,0 +1,73 @@
package mcpTool
import (
"context"
"net/url"
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
)
type exportedDictionary struct {
Name string `json:"name"`
Type string `json:"type"`
Status *bool `json:"status"`
Desc string `json:"desc"`
SysDictionaryDetails []system.SysDictionaryDetail `json:"sysDictionaryDetails"`
}
func fetchDictionaryList(ctx context.Context, keyword string) ([]system.SysDictionary, error) {
query := url.Values{}
if keyword != "" {
query.Set("name", keyword)
}
resp, err := getUpstream[[]system.SysDictionary](ctx, "/sysDictionary/getSysDictionaryList", query)
if err != nil {
return nil, err
}
return resp.Data, nil
}
func findDictionaryByType(ctx context.Context, dictType string) (*system.SysDictionary, error) {
dictionaries, err := fetchDictionaryList(ctx, dictType)
if err != nil {
return nil, err
}
for _, dictionary := range dictionaries {
if dictionary.Type == dictType {
dict := dictionary
return &dict, nil
}
}
return nil, nil
}
func exportDictionary(ctx context.Context, id uint) (*exportedDictionary, error) {
query := url.Values{}
query.Set("id", strconv.FormatUint(uint64(id), 10))
resp, err := getUpstream[exportedDictionary](ctx, "/sysDictionary/exportSysDictionary", query)
if err != nil {
return nil, err
}
return &resp.Data, nil
}
func createDictionary(ctx context.Context, dictionary system.SysDictionary) error {
_, err := postUpstream[map[string]any](ctx, "/sysDictionary/createSysDictionary", dictionary)
return err
}
func createDictionaryDetail(ctx context.Context, detail system.SysDictionaryDetail) error {
_, err := postUpstream[map[string]any](ctx, "/sysDictionaryDetail/createSysDictionaryDetail", detail)
return err
}
func enabledBoolPointer() *bool {
return utils.Pointer(true)
}

View File

@@ -0,0 +1,139 @@
package mcpTool
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/mark3labs/mcp-go/mcp"
)
func init() {
RegisterTool(&DictionaryQuery{})
}
type DictionaryPre struct {
Type string `json:"type"`
Desc string `json:"desc"`
}
type DictionaryInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Status *bool `json:"status"`
Desc string `json:"desc"`
Details []DictionaryDetailInfo `json:"details"`
}
type DictionaryDetailInfo struct {
ID uint `json:"id"`
Label string `json:"label"`
Value string `json:"value"`
Extend string `json:"extend"`
Status *bool `json:"status"`
Sort int `json:"sort"`
}
type DictionaryQueryResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Total int `json:"total"`
Dictionaries []DictionaryInfo `json:"dictionaries"`
}
type DictionaryQuery struct{}
func (d *DictionaryQuery) New() mcp.Tool {
return mcp.NewTool("query_dictionaries",
mcp.WithDescription("查询系统中所有的字典和字典属性用于AI生成逻辑时了解可用的字典选项"),
mcp.WithString("dictType",
mcp.Description("可选:指定字典类型进行精确查询,如果不提供则返回所有字典"),
),
mcp.WithBoolean("includeDisabled",
mcp.Description("是否包含已禁用的字典和字典项默认为false只返回启用的"),
),
mcp.WithBoolean("detailsOnly",
mcp.Description("是否只返回字典详情信息不包含字典基本信息默认为false"),
),
)
}
func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
dictType := stringValue(args["dictType"])
includeDisabled, _ := args["includeDisabled"].(bool)
detailsOnly, _ := args["detailsOnly"].(bool)
dictionaries, err := fetchDictionaryList(ctx, dictType)
if err != nil {
return nil, err
}
result := make([]DictionaryInfo, 0)
for _, dictionary := range dictionaries {
if dictType != "" && dictionary.Type != dictType {
continue
}
if !includeDisabled && dictionary.Status != nil && !*dictionary.Status {
continue
}
dictInfo, err := buildDictionaryInfo(ctx, dictionary, includeDisabled)
if err != nil {
return nil, err
}
result = append(result, dictInfo)
}
if detailsOnly {
details := make([]DictionaryDetailInfo, 0)
for _, dictionary := range result {
details = append(details, dictionary.Details...)
}
return textResultWithJSON("", map[string]any{
"success": true,
"message": "查询字典详情成功",
"total": len(details),
"details": details,
})
}
return textResultWithJSON("", DictionaryQueryResponse{
Success: true,
Message: "查询字典成功",
Total: len(result),
Dictionaries: result,
})
}
func buildDictionaryInfo(ctx context.Context, dictionary system.SysDictionary, includeDisabled bool) (DictionaryInfo, error) {
exported, err := exportDictionary(ctx, dictionary.ID)
if err != nil {
return DictionaryInfo{}, err
}
info := DictionaryInfo{
ID: dictionary.ID,
Name: exported.Name,
Type: exported.Type,
Status: exported.Status,
Desc: exported.Desc,
}
for _, detail := range exported.SysDictionaryDetails {
if !includeDisabled && detail.Status != nil && !*detail.Status {
continue
}
info.Details = append(info.Details, DictionaryDetailInfo{
ID: detail.ID,
Label: detail.Label,
Value: detail.Value,
Extend: detail.Extend,
Status: detail.Status,
Sort: detail.Sort,
})
}
return info, nil
}

31
server/mcp/enter.go Normal file
View File

@@ -0,0 +1,31 @@
package mcpTool
import (
"context"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// McpTool 定义了MCP工具必须实现的接口
type McpTool interface {
// Handle 返回工具调用信息
Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
// New 返回工具注册信息
New() mcp.Tool
}
// 工具注册表
var toolRegister = make(map[string]McpTool)
// RegisterTool 供工具在init时调用将自己注册到工具注册表中
func RegisterTool(tool McpTool) {
mcpTool := tool.New()
toolRegister[mcpTool.Name] = tool
}
// RegisterAllTools 将所有注册的工具注册到MCP服务中
func RegisterAllTools(mcpServer *server.MCPServer) {
for _, tool := range toolRegister {
mcpServer.AddTool(tool.New(), tool.Handle)
}
}

494
server/mcp/gva_analyze.go Normal file
View File

@@ -0,0 +1,494 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/mark3labs/mcp-go/mcp"
)
// 注册工具
func init() {
RegisterTool(&GVAAnalyzer{})
}
// GVAAnalyzer GVA分析器 - 用于分析当前功能是否需要创建独立的package和module
type GVAAnalyzer struct{}
// AnalyzeRequest 分析请求结构体
type AnalyzeRequest struct {
Requirement string `json:"requirement" binding:"required"` // 用户需求描述
}
// AnalyzeResponse 分析响应结构体
type AnalyzeResponse struct {
ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息
PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息
Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息
CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有)
}
// ModuleInfo 模块信息
type ModuleInfo struct {
ModuleName string `json:"moduleName"` // 模块名称
PackageName string `json:"packageName"` // 包名
Template string `json:"template"` // 模板类型
StructName string `json:"structName"` // 结构体名称
TableName string `json:"tableName"` // 表名
Description string `json:"description"` // 描述
FilePaths []string `json:"filePaths"` // 相关文件路径
}
// PackageInfo 包信息
type PackageInfo struct {
PackageName string `json:"packageName"` // 包名
Template string `json:"template"` // 模板类型
Label string `json:"label"` // 标签
Desc string `json:"desc"` // 描述
Module string `json:"module"` // 模块
IsEmpty bool `json:"isEmpty"` // 是否为空包
}
// PredesignedModuleInfo 预设计模块信息
type PredesignedModuleInfo struct {
ModuleName string `json:"moduleName"` // 模块名称
PackageName string `json:"packageName"` // 包名
Template string `json:"template"` // 模板类型
FilePaths []string `json:"filePaths"` // 文件路径列表
Description string `json:"description"` // 描述
}
// CleanupInfo 清理信息
type CleanupInfo struct {
DeletedPackages []string `json:"deletedPackages"` // 已删除的包
DeletedModules []string `json:"deletedModules"` // 已删除的模块
CleanupMessage string `json:"cleanupMessage"` // 清理消息
}
// New 创建GVA分析器工具
func (g *GVAAnalyzer) New() mcp.Tool {
return mcp.NewTool("gva_analyze",
mcp.WithDescription("返回当前系统中有效的包和模块信息,并分析用户需求是否需要创建新的包、模块和字典。同时检查并清理空包,确保系统整洁。"),
mcp.WithString("requirement",
mcp.Description("用户需求描述,用于分析是否需要创建新的包和模块"),
mcp.Required(),
),
)
}
// Handle 处理分析请求
func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 解析请求参数
requirementStr, ok := request.GetArguments()["requirement"].(string)
if !ok || requirementStr == "" {
return nil, errors.New("参数错误requirement 必须是非空字符串")
}
// 创建分析请求
analyzeReq := AnalyzeRequest{
Requirement: requirementStr,
}
// 执行分析逻辑
response, err := g.performAnalysis(ctx, analyzeReq)
if err != nil {
return nil, fmt.Errorf("分析失败: %v", err)
}
// 序列化响应
responseJSON, err := json.Marshal(response)
if err != nil {
return nil, fmt.Errorf("序列化响应失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(string(responseJSON)),
},
}, nil
}
// performAnalysis 执行分析逻辑
func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) {
_ = req
packages, err := fetchAutoCodePackages(ctx)
if err != nil {
return nil, fmt.Errorf("获取包信息失败: %v", err)
}
histories, err := fetchAutoCodeHistories(ctx)
if err != nil {
return nil, fmt.Errorf("获取历史记录失败: %v", err)
}
cleanupInfo := &CleanupInfo{
DeletedPackages: []string{},
DeletedModules: []string{},
}
validPackages := make([]PackageInfo, 0, len(packages))
var emptyPackageHistoryIDs []uint
for _, pkg := range packages {
isEmpty, err := g.isPackageFolderEmpty(pkg.PackageName, pkg.Template)
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err))
continue
}
if isEmpty {
if err := g.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err))
} else {
cleanupInfo.DeletedPackages = append(cleanupInfo.DeletedPackages, pkg.PackageName)
}
if err := deleteAutoCodePackage(ctx, pkg.ID); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err))
}
for _, history := range histories {
if history.Package == pkg.PackageName {
emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID)
cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName)
}
}
continue
}
validPackages = append(validPackages, PackageInfo{
PackageName: pkg.PackageName,
Template: pkg.Template,
Label: pkg.Label,
Desc: pkg.Desc,
Module: pkg.Module,
IsEmpty: false,
})
}
var dirtyHistoryIDs []uint
for _, history := range histories {
for _, emptyID := range emptyPackageHistoryIDs {
if history.ID == emptyID {
dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID)
break
}
}
}
if len(dirtyHistoryIDs) > 0 {
deletedCount := 0
for _, historyID := range dirtyHistoryIDs {
if err := deleteAutoCodeHistory(ctx, historyID); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err))
continue
}
deletedCount++
}
if deletedCount > 0 {
global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", deletedCount))
}
if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err))
}
}
predesignedModules, err := g.scanPredesignedModules()
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err))
predesignedModules = []PredesignedModuleInfo{}
}
filteredModules := []PredesignedModuleInfo{}
for _, module := range predesignedModules {
isDeleted := false
for _, deletedPkg := range cleanupInfo.DeletedPackages {
if module.PackageName == deletedPkg {
isDeleted = true
break
}
}
if !isDeleted {
filteredModules = append(filteredModules, module)
}
}
dictionaries := []DictionaryPre{}
dictEntities, err := fetchDictionaryList(ctx, "")
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err))
} else {
for _, dictionary := range dictEntities {
dictionaries = append(dictionaries, DictionaryPre{
Type: dictionary.Type,
Desc: dictionary.Desc,
})
}
}
var cleanupResult *CleanupInfo
if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 {
var message strings.Builder
message.WriteString("**系统清理完成**\n\n")
if len(cleanupInfo.DeletedPackages) > 0 {
message.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", ")))
}
if len(cleanupInfo.DeletedModules) > 0 {
message.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", ")))
}
cleanupInfo.CleanupMessage = message.String()
cleanupResult = cleanupInfo
}
response := &AnalyzeResponse{
ExistingPackages: validPackages,
PredesignedModules: filteredModules,
Dictionaries: dictionaries,
CleanupInfo: cleanupResult,
}
return response, nil
}
// isPackageFolderEmpty 检查包文件夹是否为空
func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) {
// 根据模板类型确定基础路径
var basePath string
if template == "plugin" {
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
} else {
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName)
}
// 检查文件夹是否存在
if _, err := os.Stat(basePath); os.IsNotExist(err) {
return true, nil // 文件夹不存在,认为空
} else if err != nil {
return false, err // 其他错误
}
// 递归检查是否有.go文件
return g.hasGoFilesRecursive(basePath)
}
// hasGoFilesRecursive 递归检查目录及其子目录中是否有.go文件
func (g *GVAAnalyzer) hasGoFilesRecursive(dirPath string) (bool, error) {
entries, err := os.ReadDir(dirPath)
if err != nil {
return true, err // 读取失败,返回空
}
// 检查当前目录下的.go文件
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
return false, nil // 找到.go文件不为空
}
}
// 递归检查子目录
for _, entry := range entries {
if entry.IsDir() {
subDirPath := filepath.Join(dirPath, entry.Name())
isEmpty, err := g.hasGoFilesRecursive(subDirPath)
if err != nil {
continue // 忽略子目录的错误,继续检查其他目录
}
if !isEmpty {
return false, nil // 子目录中找到.go文件不为空
}
}
}
return true, nil // 没有找到.go文件为空
}
// removeEmptyPackageFolder 删除空包文件夹
func (g *GVAAnalyzer) removeEmptyPackageFolder(packageName, template string) error {
var basePath string
if template == "plugin" {
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
} else {
// 对于package类型需要删除多个目录
paths := []string{
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName),
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName),
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName),
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName),
}
for _, path := range paths {
if err := g.removeDirectoryIfExists(path); err != nil {
return err
}
}
return nil
}
return g.removeDirectoryIfExists(basePath)
}
// removeDirectoryIfExists 删除目录(如果存在)
func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error {
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
return nil // 目录不存在,无需删除
} else if err != nil {
return err // 其他错误
}
// 检查目录中是否包含go文件
noGoFiles, err := g.hasGoFilesRecursive(dirPath)
if err != nil {
return err
}
// hasGoFilesRecursive 返回 false 表示发现了 go 文件
if noGoFiles {
return os.RemoveAll(dirPath)
}
return nil
}
// cleanupRelatedApiAndMenus 清理相关的API和菜单记录
func (g *GVAAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error {
if len(historyIDs) == 0 {
return nil
}
// 这里可以根据需要实现具体的API和菜单清理逻辑
// 由于涉及到具体的业务逻辑,这里只做日志记录
global.GVA_LOG.Info(fmt.Sprintf("清理历史记录ID %v 相关的API和菜单记录", historyIDs))
// 可以调用service层的相关方法进行清理
// 例如service.ServiceGroupApp.SystemApiService.DeleteApisByIds(historyIDs)
// 例如service.ServiceGroupApp.MenuService.DeleteMenusByIds(historyIDs)
return nil
}
// scanPredesignedModules 扫描预设计模块
func (g *GVAAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) {
// 获取autocode配置路径
autocodeRoot := global.GVA_CONFIG.AutoCode.Root
if autocodeRoot == "" {
return nil, errors.New("autocode根路径未配置")
}
var modules []PredesignedModuleInfo
// 扫描plugin目录
pluginModules, err := g.scanPluginModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "plugin"))
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描plugin模块失败: %v", err))
} else {
modules = append(modules, pluginModules...)
}
// 扫描model目录
modelModules, err := g.scanModelModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "model"))
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描model模块失败: %v", err))
} else {
modules = append(modules, modelModules...)
}
return modules, nil
}
// scanPluginModules 扫描插件模块
func (g *GVAAnalyzer) scanPluginModules(pluginDir string) ([]PredesignedModuleInfo, error) {
var modules []PredesignedModuleInfo
if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
return modules, nil // 目录不存在,返回空列表
}
entries, err := os.ReadDir(pluginDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.IsDir() {
pluginName := entry.Name()
pluginPath := filepath.Join(pluginDir, pluginName)
// 查找model目录
modelDir := filepath.Join(pluginPath, "model")
if _, err := os.Stat(modelDir); err == nil {
// 扫描model目录下的模块
pluginModules, err := g.scanModulesInDirectory(modelDir, pluginName, "plugin")
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描插件 %s 的模块失败: %v", pluginName, err))
continue
}
modules = append(modules, pluginModules...)
}
}
}
return modules, nil
}
// scanModelModules 扫描模型模块
func (g *GVAAnalyzer) scanModelModules(modelDir string) ([]PredesignedModuleInfo, error) {
var modules []PredesignedModuleInfo
if _, err := os.Stat(modelDir); os.IsNotExist(err) {
return modules, nil // 目录不存在,返回空列表
}
entries, err := os.ReadDir(modelDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.IsDir() {
packageName := entry.Name()
packagePath := filepath.Join(modelDir, packageName)
// 扫描包目录下的模块
packageModules, err := g.scanModulesInDirectory(packagePath, packageName, "package")
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描包 %s 的模块失败: %v", packageName, err))
continue
}
modules = append(modules, packageModules...)
}
}
return modules, nil
}
// scanModulesInDirectory 扫描目录中的模块
func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string) ([]PredesignedModuleInfo, error) {
var modules []PredesignedModuleInfo
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
moduleName := strings.TrimSuffix(entry.Name(), ".go")
filePath := filepath.Join(dir, entry.Name())
module := PredesignedModuleInfo{
ModuleName: moduleName,
PackageName: packageName,
Template: template,
FilePaths: []string{filePath},
Description: fmt.Sprintf("%s模块中的%s", packageName, moduleName),
}
modules = append(modules, module)
}
}
return modules, nil
}

750
server/mcp/gva_execute.go Normal file
View File

@@ -0,0 +1,750 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/mark3labs/mcp-go/mcp"
)
// 注册工具
func init() {
RegisterTool(&GVAExecutor{})
}
// GVAExecutor GVA代码生成器
type GVAExecutor struct{}
// ExecuteRequest 执行请求结构体
type ExecuteRequest struct {
ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划
Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录)
}
// ExecuteResponse 执行响应结构体
type ExecuteResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
PackageID uint `json:"packageId,omitempty"`
HistoryID uint `json:"historyId,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
GeneratedPaths []string `json:"generatedPaths,omitempty"`
NextActions []string `json:"nextActions,omitempty"`
}
// ExecutionPlan 执行计划结构体
type ExecutionPlan struct {
PackageName string `json:"packageName"`
PackageType string `json:"packageType"` // "plugin" 或 "package"
NeedCreatedPackage bool `json:"needCreatedPackage"`
NeedCreatedModules bool `json:"needCreatedModules"`
NeedCreatedDictionaries bool `json:"needCreatedDictionaries"`
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"`
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
DictionariesInfo []*DictionaryGenerateRequest `json:"dictionariesInfo,omitempty"`
}
// New 创建GVA代码生成执行器工具
func (g *GVAExecutor) New() mcp.Tool {
return mcp.NewTool("gva_execute",
mcp.WithDescription(`**GVA代码生成执行器直接执行代码生成无需确认步骤**
**核心功能:**
根据需求分析和当前的包信息判断是否调用,直接生成代码。支持批量创建多个模块、自动创建包、模块、字典等。
**使用场景:**
在gva_analyze获取了当前的包信息和字典信息之后如果已经包含了可以使用的包和模块那就不要调用本mcp。根据分析结果直接生成代码适用于自动化代码生成流程。
**重要提示:**
- 当needCreatedModules=true时模块创建会自动生成API和菜单不应再调用api_creator和menu_creator工具
- 字段使用字典类型时,系统会自动检查并创建字典
- 字典创建会在模块创建之前执行
- 当字段配置了dataSource且association=2一对多关联系统会自动将fieldType修改为'array'`),
mcp.WithObject("executionPlan",
mcp.Description("执行计划,包含包信息、模块与字典信息"),
mcp.Required(),
mcp.Properties(map[string]interface{}{
"packageName": map[string]interface{}{
"type": "string",
"description": "包名(小写开头)",
},
"packageType": map[string]interface{}{
"type": "string",
"description": "package 或 plugin如果用户提到了使用插件则创建plugin如果用户没有特定说明则一律选用package",
"enum": []string{"package", "plugin"},
},
"needCreatedPackage": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建包为true时packageInfo必需",
},
"needCreatedModules": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建模块为true时modulesInfo必需",
},
"needCreatedDictionaries": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建字典为true时dictionariesInfo必需",
},
"packageInfo": map[string]interface{}{
"type": "object",
"description": "包创建信息当needCreatedPackage=true时必需",
"properties": map[string]interface{}{
"desc": map[string]interface{}{"type": "string", "description": "包描述"},
"label": map[string]interface{}{"type": "string", "description": "展示名"},
"template": map[string]interface{}{"type": "string", "description": "package 或 plugin如果用户提到了使用插件则创建plugin如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}},
"packageName": map[string]interface{}{"type": "string", "description": "包名"},
},
},
"modulesInfo": map[string]interface{}{
"type": "array",
"description": "模块配置列表,支持批量创建多个模块",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"package": map[string]interface{}{"type": "string", "description": "包名(小写开头,示例: userInfo"},
"tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名法,示例:user_info"},
"businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"},
"structName": map[string]interface{}{"type": "string", "description": "结构体名(大驼峰示例:UserInfo"},
"packageName": map[string]interface{}{"type": "string", "description": "文件名称"},
"description": map[string]interface{}{"type": "string", "description": "中文描述"},
"abbreviation": map[string]interface{}{"type": "string", "description": "简称"},
"humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰),一般是结构体名的小驼峰示例:userInfo"},
"gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型固定为true自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段"},
"autoMigrate": map[string]interface{}{"type": "boolean", "description": "是否自动迁移数据库"},
"autoCreateResource": map[string]interface{}{"type": "boolean", "description": "是否创建资源默认为false"},
"autoCreateApiToSql": map[string]interface{}{"type": "boolean", "description": "是否创建API默认为true"},
"autoCreateMenuToSql": map[string]interface{}{"type": "boolean", "description": "是否创建菜单默认为true"},
"autoCreateBtnAuth": map[string]interface{}{"type": "boolean", "description": "是否创建按钮权限默认为false"},
"onlyTemplate": map[string]interface{}{"type": "boolean", "description": "是否仅模板默认为false"},
"isTree": map[string]interface{}{"type": "boolean", "description": "是否树形结构默认为false"},
"treeJson": map[string]interface{}{"type": "string", "description": "树形JSON字段"},
"isAdd": map[string]interface{}{"type": "boolean", "description": "是否新增固定为false"},
"generateWeb": map[string]interface{}{"type": "boolean", "description": "是否生成前端代码"},
"generateServer": map[string]interface{}{"type": "boolean", "description": "是否生成后端代码"},
"fields": map[string]interface{}{
"type": "array",
"description": "字段列表",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"fieldName": map[string]interface{}{"type": "string", "description": "字段名(必须大写开头示例:UserName"},
"fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述"},
"fieldType": map[string]interface{}{"type": "string", "description": "字段类型string字符串、richtext富文本、int整型、bool布尔值、float64浮点型、time.Time时间、enum枚举、picture单图片、pictures多图片、video视频、file文件、jsonJSON、array数组"},
"fieldJson": map[string]interface{}{"type": "string", "description": "JSON标签,示例: userName"},
"dataTypeLong": map[string]interface{}{"type": "string", "description": "数据长度"},
"comment": map[string]interface{}{"type": "string", "description": "注释"},
"columnName": map[string]interface{}{"type": "string", "description": "数据库列名,示例: user_name"},
"fieldSearchType": map[string]interface{}{"type": "string", "description": "搜索类型:=、!=、>、>=、<、<=、LIKE、BETWEEN、IN、NOT IN、NOT BETWEEN"},
"fieldSearchHide": map[string]interface{}{"type": "boolean", "description": "是否隐藏搜索"},
"dictType": map[string]interface{}{"type": "string", "description": "字典类型,使用字典类型时系统会自动检查并创建字典"},
"form": map[string]interface{}{"type": "boolean", "description": "表单显示"},
"table": map[string]interface{}{"type": "boolean", "description": "表格显示"},
"desc": map[string]interface{}{"type": "boolean", "description": "详情显示"},
"excel": map[string]interface{}{"type": "boolean", "description": "导入导出"},
"require": map[string]interface{}{"type": "boolean", "description": "是否必填"},
"defaultValue": map[string]interface{}{"type": "string", "description": "默认值"},
"errorText": map[string]interface{}{"type": "string", "description": "错误提示"},
"clearable": map[string]interface{}{"type": "boolean", "description": "是否可清空"},
"sort": map[string]interface{}{"type": "boolean", "description": "是否排序"},
"primaryKey": map[string]interface{}{"type": "boolean", "description": "是否主键gvaModel=false时必须有一个字段为true"},
"dataSource": map[string]interface{}{
"type": "object",
"description": "数据源配置,用于配置字段的关联表信息。获取表名提示:可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名(如 SysUser 的表名为 sys_users。获取数据库名提示主数据库通常使用 gva默认数据库标识多数据库可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段,如果用户未提及关联多数据库信息则使用默认数据库,默认数据库的情况下 dbName填写为空",
"properties": map[string]interface{}{
"dbName": map[string]interface{}{"type": "string", "description": "关联的数据库名称(默认数据库留空)"},
"table": map[string]interface{}{"type": "string", "description": "关联的表名"},
"label": map[string]interface{}{"type": "string", "description": "用于显示的字段名如name、title等"},
"value": map[string]interface{}{"type": "string", "description": "用于存储的值字段名通常是id"},
"association": map[string]interface{}{"type": "integer", "description": "关联关系类型1=一对一关联2=一对多关联。一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个则选用一对一,如果他需要关联多个他的关联实体则选用一对多"},
"hasDeletedAt": map[string]interface{}{"type": "boolean", "description": "关联表是否有软删除字段"},
},
},
"checkDataSource": map[string]interface{}{"type": "boolean", "description": "是否检查数据源,启用后会验证关联表的存在性"},
"fieldIndexType": map[string]interface{}{"type": "string", "description": "索引类型"},
},
},
},
},
},
},
"paths": map[string]interface{}{
"type": "object",
"description": "生成的文件路径映射",
"additionalProperties": map[string]interface{}{"type": "string"},
},
"dictionariesInfo": map[string]interface{}{
"type": "array",
"description": "字典创建信息,字典创建会在模块创建之前执行",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"dictType": map[string]interface{}{"type": "string", "description": "字典类型,用于标识字典的唯一性"},
"dictName": map[string]interface{}{"type": "string", "description": "字典名称,必须生成,字典的中文名称"},
"description": map[string]interface{}{"type": "string", "description": "字典描述,字典的用途说明"},
"status": map[string]interface{}{"type": "boolean", "description": "字典状态true启用false禁用"},
"fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述用于AI理解字段含义并生成合适的选项"},
"options": map[string]interface{}{
"type": "array",
"description": "字典选项列表可选如果不提供将根据fieldDesc自动生成默认选项",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"label": map[string]interface{}{"type": "string", "description": "显示名称,用户看到的选项名"},
"value": map[string]interface{}{"type": "string", "description": "选项值,实际存储的值"},
"sort": map[string]interface{}{"type": "integer", "description": "排序号,数字越小越靠前"},
},
},
},
},
},
},
}),
mcp.AdditionalProperties(false),
),
mcp.WithString("requirement",
mcp.Description("原始需求描述(可选,用于日志记录)"),
),
)
}
// Handle 处理执行请求(移除确认步骤)
func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
executionPlanData, ok := request.GetArguments()["executionPlan"]
if !ok {
return nil, errors.New("参数错误executionPlan 必须提供")
}
// 解析执行计划
planJSON, err := json.Marshal(executionPlanData)
if err != nil {
return nil, fmt.Errorf("解析执行计划失败: %v", err)
}
var plan ExecutionPlan
err = json.Unmarshal(planJSON, &plan)
if err != nil {
return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确参考工具描述中的结构体格式要求", err)
}
// 验证执行计划的完整性
if err := g.validateExecutionPlan(&plan); err != nil {
return nil, fmt.Errorf("执行计划验证失败: %v", err)
}
// 获取原始需求(可选)
var originalRequirement string
if reqData, ok := request.GetArguments()["requirement"]; ok {
if reqStr, ok := reqData.(string); ok {
originalRequirement = reqStr
}
}
// 直接执行创建操作(无确认步骤)
result := g.executeCreation(ctx, &plan)
// 如果执行成功且有原始需求,提供代码复检建议
var reviewMessage string
if result.Success && originalRequirement != "" {
global.GVA_LOG.Info("执行完成返回生成的文件路径供AI进行代码复检...")
// 构建文件路径信息供AI使用
var pathsInfo []string
for _, path := range result.GeneratedPaths {
pathsInfo = append(pathsInfo, fmt.Sprintf("- %s", path))
}
reviewMessage = fmt.Sprintf("\n\n📁 已生成以下文件:\n%s\n\n💡 提示:可以检查生成的代码是否满足原始需求。", strings.Join(pathsInfo, "\n"))
} else if originalRequirement == "" {
reviewMessage = "\n\n💡 提示:如需代码复检,请提供原始需求描述。"
}
// 序列化响应
response := ExecuteResponse{
Success: result.Success,
Message: result.Message,
PackageID: result.PackageID,
HistoryID: result.HistoryID,
Paths: result.Paths,
GeneratedPaths: result.GeneratedPaths,
NextActions: result.NextActions,
}
responseJSON, err := json.MarshalIndent(response, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf("执行结果:\n\n%s%s", string(responseJSON), reviewMessage)),
},
}, nil
}
// validateExecutionPlan 验证执行计划的完整性
func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
if plan.PackageName == "" {
return errors.New("packageName 不能为空")
}
if plan.PackageType != "package" && plan.PackageType != "plugin" {
return errors.New("packageType 必须是 'package' 或 'plugin'")
}
if plan.NeedCreatedPackage && plan.PackageInfo != nil && plan.PackageType != plan.PackageInfo.Template {
return errors.New("packageType 和 packageInfo.template 必须保持一致")
}
if plan.NeedCreatedPackage {
if plan.PackageInfo == nil {
return errors.New("当 needCreatedPackage=true 时packageInfo 不能为空")
}
if plan.PackageInfo.PackageName == "" {
return errors.New("packageInfo.packageName 不能为空")
}
if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" {
return errors.New("packageInfo.template 必须是 'package' 或 'plugin'")
}
if plan.PackageInfo.Label == "" {
return errors.New("packageInfo.label 不能为空")
}
if plan.PackageInfo.Desc == "" {
return errors.New("packageInfo.desc 不能为空")
}
}
if plan.NeedCreatedModules {
if len(plan.ModulesInfo) == 0 {
return errors.New("当 needCreatedModules=true 时modulesInfo 不能为空")
}
for moduleIndex, moduleInfo := range plan.ModulesInfo {
if moduleInfo.Package == "" {
return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1)
}
if moduleInfo.StructName == "" {
return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1)
}
if moduleInfo.TableName == "" {
return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1)
}
if moduleInfo.Description == "" {
return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1)
}
if moduleInfo.Abbreviation == "" {
return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1)
}
if moduleInfo.PackageName == "" {
return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1)
}
if moduleInfo.HumpPackageName == "" {
return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1)
}
if len(moduleInfo.Fields) == 0 {
return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1)
}
for i, field := range moduleInfo.Fields {
if field.FieldName == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1)
}
if len(field.FieldName) > 0 {
firstChar := string(field.FieldName[0])
if firstChar >= "a" && firstChar <= "z" {
moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:]
}
}
if field.FieldDesc == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1)
}
if field.FieldType == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1)
}
if field.FieldJson == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1)
}
if field.ColumnName == "" {
return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1)
}
validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"}
validType := false
for _, validFieldType := range validFieldTypes {
if field.FieldType == validFieldType {
validType = true
break
}
}
if !validType {
return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持", moduleIndex+1, i+1, field.FieldType)
}
if field.FieldSearchType != "" {
validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"}
validSearchType := false
for _, validSearchTypeValue := range validSearchTypes {
if field.FieldSearchType == validSearchTypeValue {
validSearchType = true
break
}
}
if !validSearchType {
return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持", moduleIndex+1, i+1, field.FieldSearchType)
}
}
if field.DataSource != nil {
associationValue := field.DataSource.Association
if associationValue == 2 && field.FieldType != "array" {
global.GVA_LOG.Info(fmt.Sprintf("module %d field %d association=2, force fieldType to array", moduleIndex+1, i+1))
moduleInfo.Fields[i].FieldType = "array"
}
if associationValue != 1 && associationValue != 2 {
return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1 或 2", moduleIndex+1, i+1)
}
}
}
if !moduleInfo.GvaModel {
primaryKeyCount := 0
for _, field := range moduleInfo.Fields {
if field.PrimaryKey {
primaryKeyCount++
}
}
if primaryKeyCount == 0 {
return fmt.Errorf("模块 %d当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1)
}
if primaryKeyCount > 1 {
return fmt.Errorf("模块 %d当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1)
}
} else {
for i, field := range moduleInfo.Fields {
if field.PrimaryKey {
return fmt.Errorf("模块 %d当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false", moduleIndex+1, i+1)
}
}
}
}
}
return nil
}
// executeCreation 执行创建操作
func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecuteResponse {
result := &ExecuteResponse{
Success: false,
Paths: make(map[string]string),
GeneratedPaths: []string{}, // 初始化生成文件路径列表
}
// 无论如何都先构建目录结构信息确保paths始终返回
result.Paths = g.buildDirectoryStructure(plan)
// 记录预期生成的文件路径
result.GeneratedPaths = g.collectExpectedFilePaths(plan)
if !plan.NeedCreatedModules {
result.Success = true
result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; "
return result
}
// 创建包(如果需要)
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
err := createAutoCodePackage(ctx, plan.PackageInfo)
if err != nil {
result.Message = fmt.Sprintf("创建包失败: %v", err)
// 即使创建包失败也要返回paths信息
return result
}
result.Message += "包创建成功; "
}
// 创建指定字典(如果需要)
if plan.NeedCreatedDictionaries && len(plan.DictionariesInfo) > 0 {
dictResult := g.createDictionariesFromInfo(ctx, plan.DictionariesInfo)
result.Message += dictResult
}
// 批量创建字典和模块(如果需要)
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
// 遍历所有模块进行创建
for _, moduleInfo := range plan.ModulesInfo {
// 创建模块
err := moduleInfo.Pretreatment()
if err != nil {
result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err)
continue // 继续处理下一个模块
}
err = createAutoCodeModule(ctx, *moduleInfo)
if err != nil {
result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err)
continue // 继续处理下一个模块
}
result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName)
}
result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo))
// 添加重要提醒不要使用其他MCP工具
result.Message += "\n\n⚠ 重要提醒:\n"
result.Message += "模块创建已完成API和菜单已自动生成。请不要再调用以下MCP工具\n"
result.Message += "- api_creatorAPI权限已在模块创建时自动生成\n"
result.Message += "- menu_creator前端菜单已在模块创建时自动生成\n"
result.Message += "如需修改API或菜单请直接在系统管理界面中进行配置。\n"
}
result.Message += "已构建目录结构信息; "
result.Success = true
if result.Message == "" {
result.Message = "执行计划完成"
}
return result
}
// buildDirectoryStructure 构建目录结构信息
func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]string {
paths := make(map[string]string)
// 获取配置信息
autoCodeConfig := global.GVA_CONFIG.AutoCode
// 构建基础路径
rootPath := autoCodeConfig.Root
serverPath := autoCodeConfig.Server
webPath := autoCodeConfig.Web
moduleName := autoCodeConfig.Module
// 如果计划中有包名,使用计划中的包名,否则使用默认
packageName := "example"
if plan.PackageName != "" {
packageName = plan.PackageName
}
// 如果计划中有模块信息,获取第一个模块的结构名作为默认值
structName := "ExampleStruct"
if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" {
structName = plan.ModulesInfo[0].StructName
}
// 根据包类型构建不同的路径结构
packageType := plan.PackageType
if packageType == "" {
packageType = "package" // 默认为package模式
}
// 构建服务端路径
if serverPath != "" {
serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath)
if packageType == "plugin" {
// Plugin 模式:所有文件都在 /plugin/packageName/ 目录中
plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName)
// API 路径
paths["api"] = fmt.Sprintf("%s/api", plugingBasePath)
// Service 路径
paths["service"] = fmt.Sprintf("%s/service", plugingBasePath)
// Model 路径
paths["model"] = fmt.Sprintf("%s/model", plugingBasePath)
// Router 路径
paths["router"] = fmt.Sprintf("%s/router", plugingBasePath)
// Request 路径
paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath)
// Response 路径
paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath)
// Plugin 特有文件
paths["plugin_main"] = fmt.Sprintf("%s/main.go", plugingBasePath)
paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", plugingBasePath)
paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", plugingBasePath)
} else {
// Package 模式:传统的目录结构
// API 路径
paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName)
// Service 路径
paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName)
// Model 路径
paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName)
// Router 路径
paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName)
// Request 路径
paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName)
// Response 路径
paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName)
}
}
// 构建前端路径(两种模式相同)
if webPath != "" {
webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath)
if packageType == "plugin" {
// Plugin 模式:前端文件也在 /plugin/packageName/ 目录中
pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName)
// Vue 页面路径
paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath)
// API 路径
paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath)
} else {
// Package 模式:传统的目录结构
// Vue 页面路径
paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName)
// API 路径
paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName)
}
}
// 添加模块信息
paths["module"] = moduleName
paths["package_name"] = packageName
paths["package_type"] = packageType
paths["struct_name"] = structName
paths["root_path"] = rootPath
paths["server_path"] = serverPath
paths["web_path"] = webPath
return paths
}
// collectExpectedFilePaths 收集预期生成的文件路径
func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string {
var paths []string
// 获取目录结构
dirPaths := g.buildDirectoryStructure(plan)
// 如果需要创建模块,添加预期的文件路径
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
for _, moduleInfo := range plan.ModulesInfo {
structName := moduleInfo.StructName
// 后端文件
if apiPath, ok := dirPaths["api"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", apiPath, strings.ToLower(structName)))
}
if servicePath, ok := dirPaths["service"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", servicePath, strings.ToLower(structName)))
}
if modelPath, ok := dirPaths["model"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", modelPath, strings.ToLower(structName)))
}
if routerPath, ok := dirPaths["router"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", routerPath, strings.ToLower(structName)))
}
if requestPath, ok := dirPaths["request"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", requestPath, strings.ToLower(structName)))
}
if responsePath, ok := dirPaths["response"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", responsePath, strings.ToLower(structName)))
}
// 前端文件
if vuePage, ok := dirPaths["vue_page"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.vue", vuePage, strings.ToLower(structName)))
}
if vueApi, ok := dirPaths["vue_api"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.js", vueApi, strings.ToLower(structName)))
}
}
}
return paths
}
// checkDictionaryExists 检查字典是否存在
func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) {
dictionary, err := findDictionaryByType(context.Background(), dictType)
if err != nil {
return false, err
}
return dictionary != nil, nil
}
// createDictionariesFromInfo 根据 DictionariesInfo 创建字典
func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string {
var messages []string
messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo)))
for _, dictInfo := range dictionariesInfo {
exists, err := g.checkDictionaryExists(dictInfo.DictType)
if err != nil {
messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err))
continue
}
if !exists {
err = createDictionary(ctx, system.SysDictionary{
Name: dictInfo.DictName,
Type: dictInfo.DictType,
Status: enabledBoolPointer(),
Desc: dictInfo.Description,
})
if err != nil {
messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err))
continue
}
messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName))
createdDict, err := findDictionaryByType(ctx, dictInfo.DictType)
if err != nil {
messages = append(messages, fmt.Sprintf("获取创建的字典失败: %v; ", err))
continue
}
if createdDict == nil {
messages = append(messages, fmt.Sprintf("获取创建的字典失败: %s; ", dictInfo.DictType))
continue
}
if len(dictInfo.Options) > 0 {
successCount := 0
for _, option := range dictInfo.Options {
dictionaryDetail := system.SysDictionaryDetail{
Label: option.Label,
Value: option.Value,
Status: enabledBoolPointer(),
Sort: option.Sort,
SysDictionaryID: int(createdDict.ID),
}
err = createDictionaryDetail(ctx, dictionaryDetail)
if err == nil {
successCount++
}
}
messages = append(messages, fmt.Sprintf("创建了 %d 个字典选项; ", successCount))
}
} else {
messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", dictInfo.DictType))
}
}
return strings.Join(messages, "")
}

170
server/mcp/gva_review.go Normal file
View File

@@ -0,0 +1,170 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/mark3labs/mcp-go/mcp"
)
// GVAReviewer GVA代码审查工具
type GVAReviewer struct{}
// init 注册工具
func init() {
RegisterTool(&GVAReviewer{})
}
// ReviewRequest 审查请求结构
type ReviewRequest struct {
UserRequirement string `json:"userRequirement"` // 经过requirement_analyze后的用户需求
GeneratedFiles []string `json:"generatedFiles"` // gva_execute创建的文件列表
}
// ReviewResponse 审查响应结构
type ReviewResponse struct {
Success bool `json:"success"` // 是否审查成功
Message string `json:"message"` // 审查结果消息
AdjustmentPrompt string `json:"adjustmentPrompt"` // 调整代码的提示
ReviewDetails string `json:"reviewDetails"` // 详细的审查结果
}
// New 创建GVA代码审查工具
func (g *GVAReviewer) New() mcp.Tool {
return mcp.NewTool("gva_review",
mcp.WithDescription(`**GVA代码审查工具 - 在gva_execute调用后使用**
**核心功能:**
- 接收经过requirement_analyze处理的用户需求和gva_execute生成的文件列表
- 分析生成的代码是否满足用户的原始需求
- 检查是否涉及到关联、交互等复杂功能
- 如果代码不满足需求提供调整建议和新的prompt
**使用场景:**
- 在gva_execute成功执行后调用
- 用于验证生成的代码是否完整满足用户需求
- 检查模块间的关联关系是否正确实现
- 发现缺失的交互功能或业务逻辑
**工作流程:**
1. 接收用户原始需求和生成的文件列表
2. 分析需求中的关键功能点
3. 检查生成的文件是否覆盖所有功能
4. 识别缺失的关联关系、交互功能等
5. 生成调整建议和新的开发prompt
**输出内容:**
- 审查结果和是否需要调整
- 详细的缺失功能分析
- 针对性的代码调整建议
- 可直接使用的开发prompt
**重要提示:**
- 本工具专门用于代码质量审查,不执行实际的代码修改
- 重点关注模块间关联、用户交互、业务流程完整性
- 提供的调整建议应该具体可执行`),
mcp.WithString("userRequirement",
mcp.Description("经过requirement_analyze处理后的用户需求描述包含详细的功能要求和字段信息"),
mcp.Required(),
),
mcp.WithString("generatedFiles",
mcp.Description("gva_execute创建的文件列表JSON字符串格式包含所有生成的后端和前端文件路径"),
mcp.Required(),
),
)
}
// Handle 处理审查请求
func (g *GVAReviewer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取用户需求
userRequirementData, ok := request.GetArguments()["userRequirement"]
if !ok {
return nil, errors.New("参数错误userRequirement 必须提供")
}
userRequirement, ok := userRequirementData.(string)
if !ok {
return nil, errors.New("参数错误userRequirement 必须是字符串类型")
}
// 获取生成的文件列表
generatedFilesData, ok := request.GetArguments()["generatedFiles"]
if !ok {
return nil, errors.New("参数错误generatedFiles 必须提供")
}
generatedFilesStr, ok := generatedFilesData.(string)
if !ok {
return nil, errors.New("参数错误generatedFiles 必须是JSON字符串")
}
// 解析JSON字符串为字符串数组
var generatedFiles []string
err := json.Unmarshal([]byte(generatedFilesStr), &generatedFiles)
if err != nil {
return nil, fmt.Errorf("解析generatedFiles失败: %v", err)
}
if len(generatedFiles) == 0 {
return nil, errors.New("参数错误generatedFiles 不能为空")
}
// 直接生成调整提示,不进行复杂分析
adjustmentPrompt := g.generateAdjustmentPrompt(userRequirement, generatedFiles)
// 构建简化的审查详情
reviewDetails := fmt.Sprintf("📋 **代码审查报告**\n\n **用户原始需求:**\n%s\n\n **已生成文件数量:** %d\n\n **建议进行代码优化和完善**", userRequirement, len(generatedFiles))
// 构建审查结果
reviewResult := &ReviewResponse{
Success: true,
Message: "代码审查完成",
AdjustmentPrompt: adjustmentPrompt,
ReviewDetails: reviewDetails,
}
// 序列化响应
responseJSON, err := json.MarshalIndent(reviewResult, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化审查结果失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf("代码审查结果:\n\n%s", string(responseJSON))),
},
}, nil
}
// generateAdjustmentPrompt 生成调整代码的提示
func (g *GVAReviewer) generateAdjustmentPrompt(userRequirement string, generatedFiles []string) string {
var prompt strings.Builder
prompt.WriteString("🔧 **代码调整指导 Prompt**\n\n")
prompt.WriteString(fmt.Sprintf("**用户的原始需求为:** %s\n\n", userRequirement))
prompt.WriteString("**经过GVA生成后的文件有如下内容**\n")
for _, file := range generatedFiles {
prompt.WriteString(fmt.Sprintf("- %s\n", file))
}
prompt.WriteString("\n")
prompt.WriteString("**请帮我优化和完善代码,确保:**\n")
prompt.WriteString("1. 代码完全满足用户的原始需求\n")
prompt.WriteString("2. 完善模块间的关联关系,确保数据一致性\n")
prompt.WriteString("3. 实现所有必要的用户交互功能\n")
prompt.WriteString("4. 保持代码的完整性和可维护性\n")
prompt.WriteString("5. 遵循GVA框架的开发规范和最佳实践\n")
prompt.WriteString("6. 确保前后端功能完整对接\n")
prompt.WriteString("7. 添加必要的错误处理和数据验证\n\n")
prompt.WriteString("8. 如果需要vue路由跳转请使用 menu_lister获取完整路由表并且路由跳转使用 router.push({\"name\":从menu_lister中获取的name})\n\n")
prompt.WriteString("9. 如果当前所有的vue页面内容无法满足需求则自行书写vue文件并且调用 menu_creator创建菜单记录\n\n")
prompt.WriteString("10. 如果需要API调用请使用 api_lister获取api表根据需求调用对应接口\n\n")
prompt.WriteString("11. 如果当前所有API无法满足则自行书写接口补全前后端代码并使用 api_creator创建api记录\n\n")
prompt.WriteString("12. 无论前后端都不要随意删除import的内容\n\n")
prompt.WriteString("**请基于用户需求和现有文件,提供完整的代码优化方案。**")
return prompt.String()
}

153
server/mcp/http_client.go Normal file
View File

@@ -0,0 +1,153 @@
package mcpTool
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
type upstreamEnvelope[T any] struct {
Code int `json:"code"`
Data T `json:"data"`
Msg string `json:"msg"`
}
func ResolveMCPServiceURL() string {
baseURL := strings.TrimSpace(global.GVA_CONFIG.MCP.BaseURL)
if baseURL != "" {
return strings.TrimRight(baseURL, "/")
}
addr := global.GVA_CONFIG.MCP.Addr
if addr <= 0 {
addr = 8889
}
path := strings.TrimSpace(global.GVA_CONFIG.MCP.Path)
if path == "" {
path = "/mcp"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return fmt.Sprintf("http://127.0.0.1:%d%s", addr, path)
}
func upstreamBaseURL() string {
baseURL := strings.TrimSpace(global.GVA_CONFIG.MCP.UpstreamBaseURL)
if baseURL != "" {
return strings.TrimRight(baseURL, "/")
}
return "http://127.0.0.1:8888"
}
func requestTimeout() time.Duration {
timeout := global.GVA_CONFIG.MCP.RequestTimeout
if timeout <= 0 {
timeout = 15
}
return time.Duration(timeout) * time.Second
}
func getUpstream[T any](ctx context.Context, endpoint string, query url.Values) (*upstreamEnvelope[T], error) {
return doUpstream[T](ctx, http.MethodGet, endpoint, query, nil)
}
func postUpstream[T any](ctx context.Context, endpoint string, body any) (*upstreamEnvelope[T], error) {
return doUpstream[T](ctx, http.MethodPost, endpoint, nil, body)
}
func deleteUpstream[T any](ctx context.Context, endpoint string, body any) (*upstreamEnvelope[T], error) {
return doUpstream[T](ctx, http.MethodDelete, endpoint, nil, body)
}
func doUpstream[T any](ctx context.Context, method, endpoint string, query url.Values, body any) (*upstreamEnvelope[T], error) {
token := authTokenFromContext(ctx)
if token == "" {
return nil, fmt.Errorf("缺少MCP鉴权请求头: %s", configuredAuthHeader())
}
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
return nil, fmt.Errorf("上游接口路径不能为空")
}
if !strings.HasPrefix(endpoint, "/") {
endpoint = "/" + endpoint
}
baseURL := upstreamBaseURL()
requestURL, err := url.Parse(baseURL + endpoint)
if err != nil {
return nil, fmt.Errorf("构建上游请求地址失败: %w", err)
}
if len(query) > 0 {
requestURL.RawQuery = query.Encode()
}
var reader io.Reader
if body != nil {
payload, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("序列化上游请求失败: %w", err)
}
reader = bytes.NewReader(payload)
}
timeoutCtx, cancel := context.WithTimeout(ctx, requestTimeout())
defer cancel()
req, err := http.NewRequestWithContext(timeoutCtx, method, requestURL.String(), reader)
if err != nil {
return nil, fmt.Errorf("创建上游请求失败: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set(configuredAuthHeader(), token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("请求上游服务失败: %w", err)
}
defer resp.Body.Close()
rawBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取上游响应失败: %w", err)
}
var result upstreamEnvelope[T]
if len(rawBody) > 0 {
if err := json.Unmarshal(rawBody, &result); err != nil {
return nil, fmt.Errorf("解析上游响应失败: %w", err)
}
}
if resp.StatusCode >= http.StatusBadRequest {
if result.Msg != "" {
return nil, errors.New(result.Msg)
}
return nil, fmt.Errorf("上游请求失败,状态码: %d", resp.StatusCode)
}
if result.Code != 0 {
if result.Msg != "" {
return nil, errors.New(result.Msg)
}
return nil, fmt.Errorf("上游请求失败,业务码: %d", result.Code)
}
return &result, nil
}

228
server/mcp/menu_creator.go Normal file
View File

@@ -0,0 +1,228 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/mark3labs/mcp-go/mcp"
)
func init() {
RegisterTool(&MenuCreator{})
}
type MenuCreateRequest struct {
ParentId uint `json:"parentId"`
Path string `json:"path"`
Name string `json:"name"`
Hidden bool `json:"hidden"`
Component string `json:"component"`
Sort int `json:"sort"`
Title string `json:"title"`
Icon string `json:"icon"`
KeepAlive bool `json:"keepAlive"`
DefaultMenu bool `json:"defaultMenu"`
CloseTab bool `json:"closeTab"`
ActiveName string `json:"activeName"`
Parameters []MenuParameterRequest `json:"parameters"`
MenuBtn []MenuButtonRequest `json:"menuBtn"`
}
type MenuParameterRequest struct {
Type string `json:"type"`
Key string `json:"key"`
Value string `json:"value"`
}
type MenuButtonRequest struct {
Name string `json:"name"`
Desc string `json:"desc"`
}
type MenuCreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
MenuID uint `json:"menuId"`
Name string `json:"name"`
Path string `json:"path"`
}
type MenuCreator struct{}
func (m *MenuCreator) New() mcp.Tool {
return mcp.NewTool("create_menu",
mcp.WithDescription(`创建前端菜单记录用于AI编辑器自动添加前端页面时自动创建对应的菜单项。
**重要限制:**
- 当使用gva_auto_generate工具且needCreatedModules=true时模块创建会自动生成菜单项不应调用此工具
- 仅在以下情况使用1) 单独创建菜单不涉及模块创建2) AI编辑器自动添加前端页面时`),
mcp.WithNumber("parentId",
mcp.Description("父菜单ID0表示根菜单"),
mcp.DefaultNumber(0),
),
mcp.WithString("path",
mcp.Required(),
mcp.Description("路由pathuserList"),
),
mcp.WithString("name",
mcp.Required(),
mcp.Description("路由name用于Vue RouteruserList"),
),
mcp.WithBoolean("hidden",
mcp.Description("是否在菜单列表中隐藏"),
),
mcp.WithString("component",
mcp.Required(),
mcp.Description("对应的前端Vue组件路径view/user/list.vue"),
),
mcp.WithNumber("sort",
mcp.Description("菜单排序号,数字越小越靠前"),
mcp.DefaultNumber(1),
),
mcp.WithString("title",
mcp.Required(),
mcp.Description("菜单显示标题"),
),
mcp.WithString("icon",
mcp.Description("菜单图标名称"),
mcp.DefaultString("menu"),
),
mcp.WithBoolean("keepAlive",
mcp.Description("是否缓存页面"),
),
mcp.WithBoolean("defaultMenu",
mcp.Description("是否是基础路由"),
),
mcp.WithBoolean("closeTab",
mcp.Description("是否自动关闭tab"),
),
mcp.WithString("activeName",
mcp.Description("高亮菜单名称"),
),
mcp.WithString("parameters",
mcp.Description("路由参数JSON字符串格式[{\"type\":\"params\",\"key\":\"id\",\"value\":\"1\"}]"),
),
mcp.WithString("menuBtn",
mcp.Description("菜单按钮JSON字符串格式[{\"name\":\"add\",\"desc\":\"新增\"}]"),
),
)
}
func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
path, ok := args["path"].(string)
if !ok || path == "" {
return nil, errors.New("path 参数是必需的")
}
name, ok := args["name"].(string)
if !ok || name == "" {
return nil, errors.New("name 参数是必需的")
}
component, ok := args["component"].(string)
if !ok || component == "" {
return nil, errors.New("component 参数是必需的")
}
title, ok := args["title"].(string)
if !ok || title == "" {
return nil, errors.New("title 参数是必需的")
}
parentID := uint(0)
if value, ok := args["parentId"].(float64); ok {
parentID = uint(value)
}
hidden, _ := args["hidden"].(bool)
sort := 1
if value, ok := args["sort"].(float64); ok {
sort = int(value)
}
icon := "menu"
if value, ok := args["icon"].(string); ok && value != "" {
icon = value
}
keepAlive, _ := args["keepAlive"].(bool)
defaultMenu, _ := args["defaultMenu"].(bool)
closeTab, _ := args["closeTab"].(bool)
activeName, _ := args["activeName"].(string)
parameters := make([]system.SysBaseMenuParameter, 0)
if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" {
var paramReqs []MenuParameterRequest
if err := json.Unmarshal([]byte(parametersStr), &paramReqs); err != nil {
return nil, fmt.Errorf("parameters 参数格式错误: %v", err)
}
for _, param := range paramReqs {
parameters = append(parameters, system.SysBaseMenuParameter{
Type: param.Type,
Key: param.Key,
Value: param.Value,
})
}
}
menuBtns := make([]system.SysBaseMenuBtn, 0)
if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" {
var buttonReqs []MenuButtonRequest
if err := json.Unmarshal([]byte(menuBtnStr), &buttonReqs); err != nil {
return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err)
}
for _, button := range buttonReqs {
menuBtns = append(menuBtns, system.SysBaseMenuBtn{
Name: button.Name,
Desc: button.Desc,
})
}
}
menu := system.SysBaseMenu{
ParentId: parentID,
Path: path,
Name: name,
Hidden: hidden,
Component: component,
Sort: sort,
Meta: system.Meta{
Title: title,
Icon: icon,
KeepAlive: keepAlive,
DefaultMenu: defaultMenu,
CloseTab: closeTab,
ActiveName: activeName,
},
Parameters: parameters,
MenuBtn: menuBtns,
}
if _, err := postUpstream[map[string]any](ctx, "/menu/addBaseMenu", menu); err != nil {
return nil, fmt.Errorf("创建菜单失败: %v", err)
}
menuID := uint(0)
if menuListResp, err := postUpstream[[]system.SysBaseMenu](ctx, "/menu/getMenuList", map[string]any{}); err == nil {
menuID = findMenuID(menuListResp.Data, name, path)
}
return textResultWithJSON("菜单创建结果:", &MenuCreateResponse{
Success: true,
Message: fmt.Sprintf("成功创建菜单 %s", title),
MenuID: menuID,
Name: name,
Path: path,
})
}
func findMenuID(menus []system.SysBaseMenu, name, path string) uint {
for _, menu := range menus {
if menu.Name == name && menu.Path == path {
return menu.ID
}
if id := findMenuID(menu.Children, name, path); id != 0 {
return id
}
}
return 0
}

59
server/mcp/menu_lister.go Normal file
View File

@@ -0,0 +1,59 @@
package mcpTool
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/mark3labs/mcp-go/mcp"
)
func init() {
RegisterTool(&MenuLister{})
}
type MenuListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Menus []system.SysBaseMenu `json:"menus"`
TotalCount int `json:"totalCount"`
Description string `json:"description"`
}
type MenuLister struct{}
func (m *MenuLister) New() mcp.Tool {
return mcp.NewTool("list_all_menus",
mcp.WithDescription(`获取系统中所有菜单信息包括菜单树结构、路由信息、组件路径等用于前端编写vue-router时正确跳转
**功能说明:**
- 返回完整的菜单树形结构
- 包含路由配置信息path、name、component
- 包含菜单元数据title、icon、keepAlive等
- 包含菜单参数和按钮配置
- 支持父子菜单关系展示
**使用场景:**
- 前端路由配置获取所有菜单信息用于配置vue-router
- 菜单权限管理:了解系统中所有可用的菜单项
- 导航组件开发:构建动态导航菜单
- 系统架构分析:了解系统的菜单结构和页面组织`),
mcp.WithString("_placeholder",
mcp.Description("占位符防止json schema校验失败"),
),
)
}
func (m *MenuLister) Handle(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
resp, err := postUpstream[[]system.SysBaseMenu](ctx, "/menu/getMenuList", map[string]any{})
if err != nil {
return nil, err
}
return textResultWithJSON("", MenuListResponse{
Success: true,
Message: "获取菜单列表成功",
Menus: resp.Data,
TotalCount: len(resp.Data),
Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息",
})
}

8
server/mcp/page.go Normal file
View File

@@ -0,0 +1,8 @@
package mcpTool
type pageResultData[T any] struct {
List T `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}

View File

@@ -0,0 +1,37 @@
//go:build !windows
package mcpTool
import (
"errors"
"os/exec"
"syscall"
)
func prepareDetachedProcess(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
}
func processExists(pid int) bool {
if pid <= 0 {
return false
}
err := syscall.Kill(pid, 0)
return err == nil || errors.Is(err, syscall.EPERM)
}
func terminateProcess(pid int) error {
if pid <= 0 {
return nil
}
err := syscall.Kill(pid, syscall.SIGTERM)
if err == nil || errors.Is(err, syscall.ESRCH) {
return nil
}
return err
}

View File

@@ -0,0 +1,58 @@
//go:build windows
package mcpTool
import (
"errors"
"os"
"os/exec"
"syscall"
"golang.org/x/sys/windows"
)
const windowsStillActive = 259
func prepareDetachedProcess(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x00000008 | 0x00000200 | 0x08000000,
}
}
func processExists(pid int) bool {
if pid <= 0 {
return false
}
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
if err != nil {
return false
}
defer windows.CloseHandle(handle)
var code uint32
if err := windows.GetExitCodeProcess(handle, &code); err != nil {
return false
}
return code == windowsStillActive
}
func terminateProcess(pid int) error {
if pid <= 0 {
return nil
}
process, err := os.FindProcess(pid)
if err != nil {
return err
}
err = process.Kill()
if err == nil || errors.Is(err, os.ErrProcessDone) {
return nil
}
return err
}

View File

@@ -0,0 +1,199 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
)
func init() {
RegisterTool(&RequirementAnalyzer{})
}
type RequirementAnalyzer struct{}
// RequirementAnalysisRequest 需求分析请求
type RequirementAnalysisRequest struct {
UserRequirement string `json:"userRequirement"`
}
// RequirementAnalysisResponse 需求分析响应
type RequirementAnalysisResponse struct {
AIPrompt string `json:"aiPrompt"` // 给AI的提示词
}
// New 返回工具注册信息
func (t *RequirementAnalyzer) New() mcp.Tool {
return mcp.NewTool("requirement_analyzer",
mcp.WithDescription(`** 智能需求分析与模块设计工具 - 首选入口工具(最高优先级)**
** 重要提示这是所有MCP工具的首选入口请优先使用**
** 核心能力:**
作为资深系统架构师,智能分析用户需求并自动设计完整的模块架构
** 核心功能:**
1. **智能需求解构**:深度分析用户需求,识别核心业务实体、业务流程、数据关系
2. **自动模块设计**:基于需求分析,智能确定需要多少个模块及各模块功能
3. **字段智能推导**:为每个模块自动设计详细字段,包含数据类型、关联关系、字典需求
4. **架构优化建议**:提供模块拆分、关联设计、扩展性等专业建议
** 输出内容:**
- 模块数量和架构设计
- 每个模块的详细字段清单
- 数据类型和关联关系设计
- 字典需求和类型定义
- 模块间关系图和扩展建议
** 适用场景:**
- 用户需求描述不完整,需要智能补全
- 复杂业务系统的模块架构设计
- 需要专业的数据库设计建议
- 想要快速搭建生产级业务系统
** 推荐工作流:**
requirement_analyzer → gva_analyze → gva_execute → 其他辅助工具
`),
mcp.WithString("userRequirement",
mcp.Required(),
mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"),
),
)
}
// Handle 处理工具调用
func (t *RequirementAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
userRequirement, ok := request.GetArguments()["userRequirement"].(string)
if !ok || userRequirement == "" {
return nil, errors.New("参数错误userRequirement 必须是非空字符串")
}
// 分析用户需求
analysisResponse, err := t.analyzeRequirement(userRequirement)
if err != nil {
return nil, fmt.Errorf("需求分析失败: %v", err)
}
// 序列化响应
responseData, err := json.Marshal(analysisResponse)
if err != nil {
return nil, fmt.Errorf("序列化响应失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(string(responseData)),
},
}, nil
}
// analyzeRequirement 分析用户需求 - 专注于AI需求传递
func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*RequirementAnalysisResponse, error) {
// 生成AI提示词 - 这是唯一功能
aiPrompt := t.generateAIPrompt(userRequirement)
return &RequirementAnalysisResponse{
AIPrompt: aiPrompt,
}, nil
}
// generateAIPrompt 生成AI提示词 - 智能分析需求并确定模块结构
func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string {
prompt := fmt.Sprintf(`# 智能需求分析与模块设计任务
## 用户原始需求
%s
## 核心任务
你需要作为一个资深的系统架构师,深度分析用户需求,智能设计出完整的模块架构。
## 分析步骤
### 第一步:需求解构分析
请仔细分析用户需求,识别出:
1. **核心业务实体**(如:用户、商品、订单、疫苗、宠物等)
2. **业务流程**(如:注册、购买、记录、管理等)
3. **数据关系**(实体间的关联关系)
4. **功能模块**(需要哪些独立的管理模块)
### 第二步:模块架构设计
基于需求分析,设计出模块架构,格式如下:
**模块1[模块名称]**
- 功能描述:[该模块的核心功能]
- 主要字段:[列出关键字段,注明数据类型]
- 关联关系:[与其他模块的关系,明确一对一/一对多]
- 字典需求:[需要哪些字典类型]
**模块2[模块名称]**
- 功能描述:[该模块的核心功能]
- 主要字段:[列出关键字段,注明数据类型]
- 关联关系:[与其他模块的关系]
- 字典需求:[需要哪些字典类型]
**...**
### 第三步:字段详细设计
为每个模块详细设计字段:
#### 模块1字段清单
- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
- 字段名2 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
- ...
#### 模块2字段清单
- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
- ...
## 智能分析指导原则
### 模块拆分原则
1. **单一职责**:每个模块只负责一个核心业务实体
2. **数据完整性**:相关数据应该在同一模块中
3. **业务独立性**:模块应该能够独立完成特定业务功能
4. **扩展性考虑**:为未来功能扩展预留空间
### 字段设计原则
1. **必要性**:只包含业务必需的字段
2. **规范性**:遵循数据库设计规范
3. **关联性**:正确识别实体间关系
4. **字典化**:状态、类型等枚举值使用字典
### 关联关系识别
- **一对一**:一个实体只能关联另一个实体的一个记录
- **一对多**:一个实体可以关联另一个实体的多个记录
- **多对多**:通过中间表实现复杂关联
## 特殊场景处理
### 复杂实体识别
当用户提到某个概念时,要判断它是否需要独立模块:
- **字典处理**:简单的常见的状态、类型(如:开关、性别、完成状态等)
- **独立模块**:复杂实体(如:疫苗管理、宠物档案、注射记录)
## 输出要求
### 必须包含的信息
1. **模块数量**:明确需要几个模块
2. **模块关系图**:用文字描述模块间关系
3. **核心字段**每个模块的关键字段至少5-10个
4. **数据类型**string、int、bool、time.Time、float64等
5. **关联设计**:明确哪些字段是关联字段
6. **字典需求**:列出需要创建的字典类型
### 严格遵循用户输入
- 如果用户提供了具体字段,**必须使用**用户提供的字段
- 如果用户提供了SQL文件**严格按照**SQL结构设计
- **不要**随意发散,**不要**添加用户未提及的功能
---
**现在请开始深度分析用户需求:"%s"**
请按照上述框架进行系统性分析,确保输出的模块设计既满足当前需求,又具备良好的扩展性。`, userRequirement, userRequirement)
return prompt
}

29
server/mcp/result.go Normal file
View File

@@ -0,0 +1,29 @@
package mcpTool
import (
"encoding/json"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
)
func textResultWithJSON(title string, payload any) (*mcp.CallToolResult, error) {
resultJSON, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %w", err)
}
text := string(resultJSON)
if title != "" {
text = fmt.Sprintf("%s\n\n%s", title, text)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: text,
},
},
}, nil
}

52
server/mcp/server.go Normal file
View File

@@ -0,0 +1,52 @@
package mcpTool
import (
"net/http"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
mcpServer "github.com/mark3labs/mcp-go/server"
)
func NewMCPServer() *mcpServer.MCPServer {
config := global.GVA_CONFIG.MCP
s := mcpServer.NewMCPServer(
config.Name,
config.Version,
)
global.GVA_MCP_SERVER = s
RegisterAllTools(s)
return s
}
func NewStreamableHTTPServer() *mcpServer.StreamableHTTPServer {
config := global.GVA_CONFIG.MCP
path := config.Path
if path == "" {
path = "/mcp"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
mux := http.NewServeMux()
httpSrv := &http.Server{
Handler: mux,
}
handler := mcpServer.NewStreamableHTTPServer(
NewMCPServer(),
mcpServer.WithHTTPContextFunc(WithHTTPRequestContext),
mcpServer.WithStreamableHTTPServer(httpSrv),
)
mux.Handle(path, handler)
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
return handler
}

View File

@@ -0,0 +1,479 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
const (
mcpRuntimeDirName = ".tmp"
mcpRuntimeSubDir = "mcp"
mcpRuntimeMetaName = "managed-process.json"
mcpRuntimeLogName = "mcp.log"
mcpHealthCheckTimeout = 2 * time.Second
mcpStartWaitTimeout = 20 * time.Second
mcpStopWaitTimeout = 8 * time.Second
mcpBuildTimeout = 2 * time.Minute
)
type ManagedStandaloneStatus struct {
State string `json:"state"`
Managed bool `json:"managed"`
Reachable bool `json:"reachable"`
Starting bool `json:"starting"`
BaseURL string `json:"baseURL"`
HealthURL string `json:"healthURL"`
ListenAddr string `json:"listenAddr"`
Path string `json:"path"`
AuthHeader string `json:"authHeader"`
PID int `json:"pid,omitempty"`
LogPath string `json:"logPath,omitempty"`
StartedAt string `json:"startedAt,omitempty"`
LastError string `json:"lastError,omitempty"`
Message string `json:"message,omitempty"`
}
type managedProcessMeta struct {
PID int `json:"pid"`
StartedAt string `json:"startedAt"`
LogPath string `json:"logPath"`
ConfigPath string `json:"configPath"`
WorkDir string `json:"workDir"`
Command string `json:"command"`
Args []string `json:"args"`
}
func ResolveMCPListenAddr() string {
addr := global.GVA_CONFIG.MCP.Addr
if addr <= 0 {
addr = 8889
}
return fmt.Sprintf(":%d", addr)
}
func ResolveMCPPath() string {
path := strings.TrimSpace(global.GVA_CONFIG.MCP.Path)
if path == "" {
path = "/mcp"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return path
}
func ResolveMCPHealthURL() string {
baseURL, err := url.Parse(ResolveMCPServiceURL())
if err != nil || baseURL.Scheme == "" || baseURL.Host == "" {
return fmt.Sprintf("http://127.0.0.1%s/health", ResolveMCPListenAddr())
}
baseURL.Path = "/health"
baseURL.RawQuery = ""
baseURL.Fragment = ""
return baseURL.String()
}
func GetManagedStandaloneStatus(ctx context.Context) ManagedStandaloneStatus {
reachable, reachErr := checkMCPHealth(ctx)
meta, _ := readManagedProcessMeta()
if meta != nil && meta.PID > 0 && !processExists(meta.PID) {
_ = removeManagedProcessMeta()
meta = nil
}
status := ManagedStandaloneStatus{
Managed: meta != nil,
Reachable: reachable,
BaseURL: ResolveMCPServiceURL(),
HealthURL: ResolveMCPHealthURL(),
ListenAddr: ResolveMCPListenAddr(),
Path: ResolveMCPPath(),
AuthHeader: ConfiguredAuthHeader(),
}
if meta != nil {
status.PID = meta.PID
status.LogPath = meta.LogPath
status.StartedAt = meta.StartedAt
}
switch {
case meta != nil && reachable:
status.State = "running"
status.Message = "MCP 独立服务运行中"
case meta != nil && processExists(meta.PID):
status.State = "starting"
status.Managed = true
status.Starting = true
status.Message = "MCP 独立服务启动中"
case reachable:
status.State = "external"
status.Managed = false
status.Message = "检测到 MCP 服务已运行,但不是由页面启动的托管进程"
case meta != nil:
status.State = "stopped"
status.Managed = false
status.Message = "上次托管的 MCP 进程已退出"
default:
status.State = "stopped"
status.Message = "MCP 独立服务未启动"
}
if !reachable && reachErr != nil && status.State != "stopped" {
status.LastError = reachErr.Error()
}
return status
}
func StartManagedStandalone(ctx context.Context) (ManagedStandaloneStatus, error) {
current := GetManagedStandaloneStatus(ctx)
if current.Reachable {
return current, nil
}
meta, _ := readManagedProcessMeta()
if meta != nil && meta.PID > 0 && processExists(meta.PID) {
return waitForManagedProcess(ctx, meta)
}
commandPath, commandArgs, workDir, configPath, err := resolveManagedStartCommand()
if err != nil {
return GetManagedStandaloneStatus(context.Background()), err
}
runtimeDir, err := ensureMCPRuntimeDir()
if err != nil {
return GetManagedStandaloneStatus(context.Background()), err
}
logPath := filepath.Join(runtimeDir, mcpRuntimeLogName)
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return GetManagedStandaloneStatus(context.Background()), err
}
defer logFile.Close()
cmd := exec.Command(commandPath, commandArgs...)
cmd.Dir = workDir
cmd.Stdout = logFile
cmd.Stderr = logFile
cmd.Env = append(os.Environ(), "GVA_MCP_CONFIG="+configPath)
prepareDetachedProcess(cmd)
if err := cmd.Start(); err != nil {
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("启动 MCP 独立服务失败: %w", err)
}
pid := cmd.Process.Pid
_ = cmd.Process.Release()
meta = &managedProcessMeta{
PID: pid,
StartedAt: time.Now().Format(time.RFC3339),
LogPath: logPath,
ConfigPath: configPath,
WorkDir: workDir,
Command: commandPath,
Args: append([]string{}, commandArgs...),
}
if err := writeManagedProcessMeta(meta); err != nil {
return GetManagedStandaloneStatus(context.Background()), err
}
return waitForManagedProcess(ctx, meta)
}
func StopManagedStandalone(ctx context.Context) (ManagedStandaloneStatus, error) {
meta, err := readManagedProcessMeta()
if err != nil {
status := GetManagedStandaloneStatus(ctx)
if status.Reachable {
return status, errors.New("当前 MCP 服务不是由页面启动的,无法自动停用")
}
return status, nil
}
if meta.PID > 0 && processExists(meta.PID) {
if err := terminateProcess(meta.PID); err != nil {
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("停用 MCP 独立服务失败: %w", err)
}
}
deadline := time.NewTimer(mcpStopWaitTimeout)
ticker := time.NewTicker(200 * time.Millisecond)
defer deadline.Stop()
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return GetManagedStandaloneStatus(context.Background()), ctx.Err()
case <-deadline.C:
_ = removeManagedProcessMeta()
return GetManagedStandaloneStatus(context.Background()), nil
case <-ticker.C:
if meta.PID <= 0 || !processExists(meta.PID) {
_ = removeManagedProcessMeta()
status := GetManagedStandaloneStatus(context.Background())
if status.Reachable {
status.State = "external"
status.Message = "托管进程已停止,但检测到还有其他 MCP 服务在运行"
}
return status, nil
}
}
}
}
func checkMCPHealth(ctx context.Context) (bool, error) {
timeoutCtx, cancel := context.WithTimeout(context.Background(), mcpHealthCheckTimeout)
defer cancel()
if ctx != nil {
if deadline, ok := ctx.Deadline(); ok {
timeoutCtx, cancel = context.WithDeadline(context.Background(), deadline)
defer cancel()
}
}
req, err := http.NewRequestWithContext(timeoutCtx, http.MethodGet, ResolveMCPHealthURL(), nil)
if err != nil {
return false, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return true, nil
}
return false, fmt.Errorf("MCP 健康检查失败: %s", resp.Status)
}
func waitForManagedProcess(ctx context.Context, meta *managedProcessMeta) (ManagedStandaloneStatus, error) {
deadline := time.NewTimer(mcpStartWaitTimeout)
ticker := time.NewTicker(300 * time.Millisecond)
defer deadline.Stop()
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return GetManagedStandaloneStatus(context.Background()), ctx.Err()
case <-deadline.C:
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("等待 MCP 独立服务启动超时,请查看日志: %s", meta.LogPath)
case <-ticker.C:
current := GetManagedStandaloneStatus(context.Background())
if current.Reachable {
return current, nil
}
if meta.PID > 0 && !processExists(meta.PID) {
return current, fmt.Errorf("MCP 独立进程已退出,请查看日志: %s", meta.LogPath)
}
}
}
}
func resolveManagedStartCommand() (string, []string, string, string, error) {
serverRoot := resolveMCPServerRoot()
if serverRoot == "" {
return "", nil, "", "", errors.New("未找到 server 根目录,无法启动 MCP 独立服务")
}
configPath, err := resolveMCPConfigPath(serverRoot)
if err != nil {
return "", nil, "", "", err
}
if explicit := strings.TrimSpace(os.Getenv("GVA_MCP_BIN")); explicit != "" {
if !fileExists(explicit) {
return "", nil, "", "", fmt.Errorf("GVA_MCP_BIN 指向的文件不存在: %s", explicit)
}
return explicit, []string{"-config", configPath}, filepath.Dir(explicit), configPath, nil
}
binaryPath, err := ensureManagedBinary(serverRoot)
if err != nil {
return "", nil, "", "", err
}
return binaryPath, []string{"-config", configPath}, serverRoot, configPath, nil
}
func ensureManagedBinary(serverRoot string) (string, error) {
runtimeDir, err := ensureMCPRuntimeDir()
if err != nil {
return "", err
}
binaryPath := filepath.Join(runtimeDir, managedBinaryName())
sourceDir := filepath.Join(serverRoot, "cmd", "mcp")
goBin, lookErr := exec.LookPath("go")
if lookErr == nil && isDir(sourceDir) {
buildCtx, cancel := context.WithTimeout(context.Background(), mcpBuildTimeout)
defer cancel()
cmd := exec.CommandContext(buildCtx, goBin, "build", "-o", binaryPath, "./cmd/mcp")
cmd.Dir = serverRoot
output, err := cmd.CombinedOutput()
if err != nil {
message := strings.TrimSpace(string(output))
if message != "" {
return "", fmt.Errorf("构建 MCP 独立服务失败: %w, 输出: %s", err, message)
}
return "", fmt.Errorf("构建 MCP 独立服务失败: %w", err)
}
return binaryPath, nil
}
if fileExists(binaryPath) {
return binaryPath, nil
}
return "", errors.New("未检测到可用的 Go 环境,且本地没有可复用的 MCP 独立二进制")
}
func resolveMCPServerRoot() string {
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
serverDir := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Server)
if serverDir == "" {
serverDir = "server"
}
candidates := []string{}
if root != "" {
candidates = append(candidates, filepath.Join(root, filepath.FromSlash(serverDir)))
candidates = append(candidates, root)
}
if cwd, err := os.Getwd(); err == nil {
candidates = append(candidates, cwd)
candidates = append(candidates, filepath.Join(cwd, "server"))
}
for _, candidate := range candidates {
if isDir(filepath.Join(candidate, "cmd", "mcp")) {
return candidate
}
}
if len(candidates) > 0 {
return candidates[0]
}
return ""
}
func resolveMCPConfigPath(serverRoot string) (string, error) {
candidates := []string{
filepath.Join(serverRoot, "cmd", "mcp", "config.yaml"),
filepath.Join(serverRoot, "config.yaml"),
}
for _, candidate := range candidates {
if fileExists(candidate) {
return candidate, nil
}
}
return "", errors.New("未找到 MCP 配置文件,请确认 cmd/mcp/config.yaml 或 server/config.yaml 存在")
}
func ensureMCPRuntimeDir() (string, error) {
runtimeDir := filepath.Join(resolveMCPProjectRoot(), mcpRuntimeDirName, mcpRuntimeSubDir)
if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
return "", err
}
return runtimeDir, nil
}
func resolveMCPProjectRoot() string {
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
if root != "" {
return root
}
serverRoot := resolveMCPServerRoot()
if serverRoot == "" {
return "."
}
if fileExists(filepath.Join(serverRoot, "go.mod")) {
return filepath.Dir(serverRoot)
}
return serverRoot
}
func readManagedProcessMeta() (*managedProcessMeta, error) {
data, err := os.ReadFile(managedProcessMetaPath())
if err != nil {
return nil, err
}
var meta managedProcessMeta
if err := json.Unmarshal(data, &meta); err != nil {
return nil, err
}
return &meta, nil
}
func writeManagedProcessMeta(meta *managedProcessMeta) error {
data, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return err
}
return os.WriteFile(managedProcessMetaPath(), data, 0o644)
}
func removeManagedProcessMeta() error {
err := os.Remove(managedProcessMetaPath())
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
func managedProcessMetaPath() string {
runtimeDir, err := ensureMCPRuntimeDir()
if err != nil {
return filepath.Join(resolveMCPProjectRoot(), mcpRuntimeDirName, mcpRuntimeSubDir, mcpRuntimeMetaName)
}
return filepath.Join(runtimeDir, mcpRuntimeMetaName)
}
func managedBinaryName() string {
if runtime.GOOS == "windows" {
return "gva-mcp-standalone.exe"
}
return "gva-mcp-standalone"
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func isDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}