| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package controller |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "strings" |
| |
| "github.com/apache/answer/internal/base/pager" |
| "github.com/apache/answer/internal/entity" |
| "github.com/apache/answer/internal/schema" |
| answercommon "github.com/apache/answer/internal/service/answer_common" |
| "github.com/apache/answer/internal/service/comment" |
| "github.com/apache/answer/internal/service/content" |
| "github.com/apache/answer/internal/service/feature_toggle" |
| questioncommon "github.com/apache/answer/internal/service/question_common" |
| "github.com/apache/answer/internal/service/siteinfo_common" |
| tagcommonser "github.com/apache/answer/internal/service/tag_common" |
| usercommon "github.com/apache/answer/internal/service/user_common" |
| "github.com/mark3labs/mcp-go/mcp" |
| "github.com/segmentfault/pacman/log" |
| ) |
| |
| type MCPController struct { |
| searchService *content.SearchService |
| siteInfoService siteinfo_common.SiteInfoCommonService |
| tagCommonService *tagcommonser.TagCommonService |
| questioncommon *questioncommon.QuestionCommon |
| commentRepo comment.CommentRepo |
| userCommon *usercommon.UserCommon |
| answerRepo answercommon.AnswerRepo |
| featureToggleSvc *feature_toggle.FeatureToggleService |
| } |
| |
| // NewMCPController new site info controller. |
| func NewMCPController( |
| searchService *content.SearchService, |
| siteInfoService siteinfo_common.SiteInfoCommonService, |
| tagCommonService *tagcommonser.TagCommonService, |
| questioncommon *questioncommon.QuestionCommon, |
| commentRepo comment.CommentRepo, |
| userCommon *usercommon.UserCommon, |
| answerRepo answercommon.AnswerRepo, |
| featureToggleSvc *feature_toggle.FeatureToggleService, |
| ) *MCPController { |
| return &MCPController{ |
| searchService: searchService, |
| siteInfoService: siteInfoService, |
| tagCommonService: tagCommonService, |
| questioncommon: questioncommon, |
| commentRepo: commentRepo, |
| userCommon: userCommon, |
| answerRepo: answerRepo, |
| featureToggleSvc: featureToggleSvc, |
| } |
| } |
| |
| func (c *MCPController) ensureMCPEnabled(ctx context.Context) error { |
| if c.featureToggleSvc == nil { |
| return nil |
| } |
| return c.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureMCP) |
| } |
| |
| func (c *MCPController) MCPQuestionsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| if err := c.ensureMCPEnabled(ctx); err != nil { |
| return nil, err |
| } |
| cond := schema.NewMCPSearchCond(request) |
| |
| siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| log.Errorf("get site general info failed: %v", err) |
| return nil, err |
| } |
| |
| searchResp, err := c.searchService.Search(ctx, &schema.SearchDTO{ |
| Query: cond.ToQueryString() + " is:question", |
| Page: 1, |
| Size: 5, |
| Order: "newest", |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| resp := make([]*schema.MCPSearchQuestionInfoResp, 0) |
| for _, question := range searchResp.SearchResults { |
| t := &schema.MCPSearchQuestionInfoResp{ |
| QuestionID: question.Object.QuestionID, |
| Title: question.Object.Title, |
| Content: question.Object.Excerpt, |
| Link: fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, question.Object.QuestionID), |
| } |
| resp = append(resp, t) |
| } |
| |
| data, _ := json.Marshal(resp) |
| return mcp.NewToolResultText(string(data)), nil |
| } |
| } |
| |
| func (c *MCPController) MCPQuestionDetailHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| if err := c.ensureMCPEnabled(ctx); err != nil { |
| return nil, err |
| } |
| cond := schema.NewMCPSearchQuestionDetail(request) |
| |
| siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| log.Errorf("get site general info failed: %v", err) |
| return nil, err |
| } |
| |
| question, err := c.questioncommon.Info(ctx, cond.QuestionID, "") |
| if err != nil { |
| log.Errorf("get question failed: %v", err) |
| return mcp.NewToolResultText("No question found."), nil |
| } |
| |
| resp := &schema.MCPSearchQuestionInfoResp{ |
| QuestionID: question.ID, |
| Title: question.Title, |
| Content: question.Content, |
| Link: fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, question.ID), |
| } |
| res, _ := json.Marshal(resp) |
| return mcp.NewToolResultText(string(res)), nil |
| } |
| } |
| |
| func (c *MCPController) MCPAnswersHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| if err := c.ensureMCPEnabled(ctx); err != nil { |
| return nil, err |
| } |
| cond := schema.NewMCPSearchAnswerCond(request) |
| |
| siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| log.Errorf("get site general info failed: %v", err) |
| return nil, err |
| } |
| |
| if len(cond.QuestionID) > 0 { |
| answerList, err := c.answerRepo.GetAnswerList(ctx, &entity.Answer{QuestionID: cond.QuestionID}) |
| if err != nil { |
| log.Errorf("get answers failed: %v", err) |
| return nil, err |
| } |
| resp := make([]*schema.MCPSearchAnswerInfoResp, 0) |
| for _, answer := range answerList { |
| t := &schema.MCPSearchAnswerInfoResp{ |
| QuestionID: answer.QuestionID, |
| AnswerID: answer.ID, |
| AnswerContent: answer.OriginalText, |
| Link: fmt.Sprintf("%s/questions/%s/answers/%s", siteGeneral.SiteUrl, answer.QuestionID, answer.ID), |
| } |
| resp = append(resp, t) |
| } |
| data, _ := json.Marshal(resp) |
| return mcp.NewToolResultText(string(data)), nil |
| } |
| |
| answerList, err := c.answerRepo.GetAnswerList(ctx, &entity.Answer{QuestionID: cond.QuestionID}) |
| if err != nil { |
| log.Errorf("get answers failed: %v", err) |
| return nil, err |
| } |
| resp := make([]*schema.MCPSearchAnswerInfoResp, 0) |
| for _, answer := range answerList { |
| t := &schema.MCPSearchAnswerInfoResp{ |
| QuestionID: answer.QuestionID, |
| AnswerID: answer.ID, |
| AnswerContent: answer.OriginalText, |
| Link: fmt.Sprintf("%s/questions/%s/answers/%s", siteGeneral.SiteUrl, answer.QuestionID, answer.ID), |
| } |
| resp = append(resp, t) |
| } |
| data, _ := json.Marshal(resp) |
| return mcp.NewToolResultText(string(data)), nil |
| } |
| } |
| |
| func (c *MCPController) MCPCommentsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| if err := c.ensureMCPEnabled(ctx); err != nil { |
| return nil, err |
| } |
| cond := schema.NewMCPSearchCommentCond(request) |
| |
| siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| log.Errorf("get site general info failed: %v", err) |
| return nil, err |
| } |
| |
| dto := &comment.CommentQuery{ |
| PageCond: pager.PageCond{Page: 1, PageSize: 5}, |
| QueryCond: "newest", |
| ObjectID: cond.ObjectID, |
| } |
| commentList, total, err := c.commentRepo.GetCommentPage(ctx, dto) |
| if err != nil { |
| return nil, err |
| } |
| if total == 0 { |
| return mcp.NewToolResultText("No comments found."), nil |
| } |
| |
| resp := make([]*schema.MCPSearchCommentInfoResp, 0) |
| for _, comment := range commentList { |
| t := &schema.MCPSearchCommentInfoResp{ |
| CommentID: comment.ID, |
| Content: comment.OriginalText, |
| ObjectID: comment.ObjectID, |
| Link: fmt.Sprintf("%s/comments/%s", siteGeneral.SiteUrl, comment.ID), |
| } |
| resp = append(resp, t) |
| } |
| data, _ := json.Marshal(resp) |
| return mcp.NewToolResultText(string(data)), nil |
| } |
| } |
| |
| func (c *MCPController) MCPTagsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| if err := c.ensureMCPEnabled(ctx); err != nil { |
| return nil, err |
| } |
| cond := schema.NewMCPSearchTagCond(request) |
| |
| siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| log.Errorf("get site general info failed: %v", err) |
| return nil, err |
| } |
| |
| tags, total, err := c.tagCommonService.GetTagPage(ctx, 1, 10, &entity.Tag{DisplayName: cond.TagName}, "newest") |
| if err != nil { |
| log.Errorf("get tags failed: %v", err) |
| return nil, err |
| } |
| |
| if total == 0 { |
| res := strings.Builder{} |
| res.WriteString("No tags found.\n") |
| return mcp.NewToolResultText(res.String()), nil |
| } |
| |
| resp := make([]*schema.MCPSearchTagResp, 0) |
| for _, tag := range tags { |
| t := &schema.MCPSearchTagResp{ |
| TagName: tag.SlugName, |
| DisplayName: tag.DisplayName, |
| Description: tag.OriginalText, |
| Link: fmt.Sprintf("%s/tags/%s", siteGeneral.SiteUrl, tag.SlugName), |
| } |
| resp = append(resp, t) |
| } |
| data, _ := json.Marshal(resp) |
| return mcp.NewToolResultText(string(data)), nil |
| } |
| } |
| |
| func (c *MCPController) MCPTagDetailsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| if err := c.ensureMCPEnabled(ctx); err != nil { |
| return nil, err |
| } |
| cond := schema.NewMCPSearchTagCond(request) |
| |
| siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| log.Errorf("get site general info failed: %v", err) |
| return nil, err |
| } |
| |
| tag, exist, err := c.tagCommonService.GetTagBySlugName(ctx, cond.TagName) |
| if err != nil { |
| log.Errorf("get tag failed: %v", err) |
| return nil, err |
| } |
| if !exist { |
| return mcp.NewToolResultText("Tag not found."), nil |
| } |
| |
| resp := &schema.MCPSearchTagResp{ |
| TagName: tag.SlugName, |
| DisplayName: tag.DisplayName, |
| Description: tag.OriginalText, |
| Link: fmt.Sprintf("%s/tags/%s", siteGeneral.SiteUrl, tag.SlugName), |
| } |
| res, _ := json.Marshal(resp) |
| return mcp.NewToolResultText(string(res)), nil |
| } |
| } |
| |
| func (c *MCPController) MCPUserDetailsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { |
| if err := c.ensureMCPEnabled(ctx); err != nil { |
| return nil, err |
| } |
| cond := schema.NewMCPSearchUserCond(request) |
| |
| siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| log.Errorf("get site general info failed: %v", err) |
| return nil, err |
| } |
| |
| user, exist, err := c.userCommon.GetUserBasicInfoByUserName(ctx, cond.Username) |
| if err != nil { |
| log.Errorf("get user failed: %v", err) |
| return nil, err |
| } |
| if !exist { |
| return mcp.NewToolResultText("User not found."), nil |
| } |
| |
| resp := &schema.MCPSearchUserInfoResp{ |
| Username: user.Username, |
| DisplayName: user.DisplayName, |
| Avatar: user.Avatar, |
| Link: fmt.Sprintf("%s/users/%s", siteGeneral.SiteUrl, user.Username), |
| } |
| res, _ := json.Marshal(resp) |
| return mcp.NewToolResultText(string(res)), nil |
| } |
| } |