@@ -31,11 +31,13 @@ import (
3131 answercommon "github.com/apache/answer/internal/service/answer_common"
3232 "github.com/apache/answer/internal/service/comment"
3333 "github.com/apache/answer/internal/service/content"
34+ "github.com/apache/answer/internal/service/embedding"
3435 "github.com/apache/answer/internal/service/feature_toggle"
3536 questioncommon "github.com/apache/answer/internal/service/question_common"
3637 "github.com/apache/answer/internal/service/siteinfo_common"
3738 tagcommonser "github.com/apache/answer/internal/service/tag_common"
3839 usercommon "github.com/apache/answer/internal/service/user_common"
40+ "github.com/apache/answer/plugin"
3941 "github.com/mark3labs/mcp-go/mcp"
4042 "github.com/segmentfault/pacman/log"
4143)
@@ -49,6 +51,7 @@ type MCPController struct {
4951 userCommon * usercommon.UserCommon
5052 answerRepo answercommon.AnswerRepo
5153 featureToggleSvc * feature_toggle.FeatureToggleService
54+ embeddingService * embedding.EmbeddingService
5255}
5356
5457// NewMCPController new site info controller.
@@ -61,6 +64,7 @@ func NewMCPController(
6164 userCommon * usercommon.UserCommon ,
6265 answerRepo answercommon.AnswerRepo ,
6366 featureToggleSvc * feature_toggle.FeatureToggleService ,
67+ embeddingService * embedding.EmbeddingService ,
6468) * MCPController {
6569 return & MCPController {
6670 searchService : searchService ,
@@ -71,6 +75,7 @@ func NewMCPController(
7175 userCommon : userCommon ,
7276 answerRepo : answerRepo ,
7377 featureToggleSvc : featureToggleSvc ,
78+ embeddingService : embeddingService ,
7479 }
7580}
7681
@@ -349,3 +354,131 @@ func (c *MCPController) MCPUserDetailsHandler() func(ctx context.Context, reques
349354 return mcp .NewToolResultText (string (res )), nil
350355 }
351356}
357+
358+ func (c * MCPController ) MCPSemanticSearchHandler () func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
359+ return func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
360+ if err := c .ensureMCPEnabled (ctx ); err != nil {
361+ return nil , err
362+ }
363+ cond := schema .NewMCPSemanticSearchCond (request )
364+ if len (cond .Query ) == 0 {
365+ return mcp .NewToolResultText ("Query is required for semantic search." ), nil
366+ }
367+
368+ siteGeneral , err := c .siteInfoService .GetSiteGeneral (ctx )
369+ if err != nil {
370+ log .Errorf ("get site general info failed: %v" , err )
371+ return nil , err
372+ }
373+
374+ results , err := c .embeddingService .SearchSimilar (ctx , cond .Query , cond .TopK )
375+ if err != nil {
376+ log .Errorf ("semantic search failed: %v" , err )
377+ return mcp .NewToolResultText ("Semantic search is not available. Embedding may not be configured." ), nil
378+ }
379+ if len (results ) == 0 {
380+ return mcp .NewToolResultText ("No semantically similar content found." ), nil
381+ }
382+
383+ resp := make ([]* schema.MCPSemanticSearchResp , 0 , len (results ))
384+ for _ , r := range results {
385+ var meta plugin.VectorSearchMetadata
386+ _ = json .Unmarshal ([]byte (r .Metadata ), & meta )
387+
388+ item := & schema.MCPSemanticSearchResp {
389+ ObjectID : r .ObjectID ,
390+ ObjectType : r .ObjectType ,
391+ Score : r .Score ,
392+ }
393+
394+ // Compose link from metadata
395+ if r .ObjectType == "answer" && meta .AnswerID != "" {
396+ item .Link = fmt .Sprintf ("%s/questions/%s/%s" , siteGeneral .SiteUrl , meta .QuestionID , meta .AnswerID )
397+ } else {
398+ item .Link = fmt .Sprintf ("%s/questions/%s" , siteGeneral .SiteUrl , meta .QuestionID )
399+ }
400+
401+ // Query content from DB using IDs stored in metadata
402+ if r .ObjectType == "question" {
403+ question , qErr := c .questioncommon .Info (ctx , meta .QuestionID , "" )
404+ if qErr != nil {
405+ log .Warnf ("get question %s for semantic search failed: %v" , meta .QuestionID , qErr )
406+ } else {
407+ item .Title = question .Title
408+ item .Content = question .Content
409+ }
410+
411+ // Fetch answers by ID from metadata
412+ for _ , a := range meta .Answers {
413+ answerEntity , exist , aErr := c .answerRepo .GetAnswer (ctx , a .AnswerID )
414+ if aErr != nil || ! exist {
415+ continue
416+ }
417+ answerItem := & schema.MCPSemanticSearchAnswer {
418+ AnswerID : a .AnswerID ,
419+ Content : answerEntity .OriginalText ,
420+ }
421+ // Fetch comments on this answer from DB
422+ for _ , ac := range a .Comments {
423+ cmt , cExist , cErr := c .commentRepo .GetComment (ctx , ac .CommentID )
424+ if cErr == nil && cExist {
425+ answerItem .Comments = append (answerItem .Comments , & schema.MCPSemanticSearchComment {
426+ CommentID : ac .CommentID ,
427+ Content : cmt .OriginalText ,
428+ })
429+ }
430+ }
431+ item .Answers = append (item .Answers , answerItem )
432+ }
433+
434+ // Fetch question comments from DB
435+ for _ , qc := range meta .Comments {
436+ cmt , cExist , cErr := c .commentRepo .GetComment (ctx , qc .CommentID )
437+ if cErr == nil && cExist {
438+ item .Comments = append (item .Comments , & schema.MCPSemanticSearchComment {
439+ CommentID : qc .CommentID ,
440+ Content : cmt .OriginalText ,
441+ })
442+ }
443+ }
444+ } else if r .ObjectType == "answer" {
445+ // Fetch question title for context
446+ question , qErr := c .questioncommon .Info (ctx , meta .QuestionID , "" )
447+ if qErr == nil {
448+ item .Title = question .Title
449+ }
450+
451+ // Fetch answer content from DB
452+ if meta .AnswerID != "" {
453+ answerEntity , exist , aErr := c .answerRepo .GetAnswer (ctx , meta .AnswerID )
454+ if aErr == nil && exist {
455+ item .Content = answerEntity .OriginalText
456+ }
457+ } else if len (meta .Answers ) > 0 {
458+ answerEntity , exist , aErr := c .answerRepo .GetAnswer (ctx , meta .Answers [0 ].AnswerID )
459+ if aErr == nil && exist {
460+ item .Content = answerEntity .OriginalText
461+ }
462+ }
463+
464+ // Fetch answer comments from DB
465+ if len (meta .Answers ) > 0 {
466+ for _ , ac := range meta .Answers [0 ].Comments {
467+ cmt , cExist , cErr := c .commentRepo .GetComment (ctx , ac .CommentID )
468+ if cErr == nil && cExist {
469+ item .Comments = append (item .Comments , & schema.MCPSemanticSearchComment {
470+ CommentID : ac .CommentID ,
471+ Content : cmt .OriginalText ,
472+ })
473+ }
474+ }
475+ }
476+ }
477+
478+ resp = append (resp , item )
479+ }
480+
481+ data , _ := json .Marshal (resp )
482+ return mcp .NewToolResultText (string (data )), nil
483+ }
484+ }
0 commit comments