AI书签管理工具开发全记录(十一):MCP集成
AI书签工具MCP集成开发摘要 本文介绍了AI书签管理工具的MCP协议集成实现。MCP是一种跨平台工具通信协议,支持大模型调用外部工具。开发采用mcp-go框架,通过stdio方式启动服务器,主要实现两大功能:1) URL智能建议获取,通过AI分析网页内容自动推荐分类和名称;2) 书签添加功能。技术实现包括:使用GORM操作数据库、OpenAI模型分析网页内容、参数校验等。该集成使工具能在支持MC
文章目录
AI书签管理工具开发全记录(十一):MCP集成
前言 📝
在上一篇文章中,我们实现了AI智能创建书签功能,大幅提升了用户操作效率。本文将聚焦于项目的另一个重要特性——MCP集成,让用户可以在支持mcp调用的客户端,例如cherry studio中对书签进行管理。
1. MCP介绍
1.1 MCP协议简介
MCP 是一种轻量级、跨平台的工具通信协议,简单的说就是大模型调用外部工具的一个统一规范,这里简要概括,后面单独写一篇文章详解。
1.2 工作流程
2. MCP服务器实现 ⚡
2.1 MCP框架选择
官方并没有为go提供MCP的SDK。不过已经有mcp-go
这样完善的第三方mcp框架了。
2.2 安装
go get github.com/mark3labs/mcp-go
看一下官方示例
package main
import (
"context"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
// Create a new MCP server
s := server.NewMCPServer(
"Demo 🚀",
"1.0.0",
server.WithToolCapabilities(false),
)
// Add tool
tool := mcp.NewTool("hello_world",
mcp.WithDescription("Say hello to someone"),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Name of the person to greet"),
),
)
// Add tool handler
s.AddTool(tool, helloHandler)
// Start the stdio server
if err := server.ServeStdio(s); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, err := request.RequireString("name")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}
运行
go run main.go
这样一个简单的MCP服务器就启动起来了,并且可以通过stdio方式接受连接。
2.2 协议选择
目前mcp支持三种协议
- stdio
- SSE
- streamable-HTTP
目前stdio最常用,考虑到我们目前书签定位是本地使用。我们采用stdio方式启动。
2.4 完整代码
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"context"
"encoding/json"
"fmt"
"log"
"regexp"
"time"
"github.com/ciclebyte/aibookmark/internal/common"
"github.com/ciclebyte/aibookmark/internal/models"
"github.com/ciclebyte/aibookmark/internal/utils"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/schema"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/spf13/cobra"
"gorm.io/gorm"
)
// mcpCmd represents the mcp command
var mcpCmd = &cobra.Command{
Use: "mcp",
Short: "启动MCP服务器",
Long: `启动MCP服务器(Stdio模式)`,
Run: func(cmd *cobra.Command, args []string) {
// 创建MCP服务器
s := server.NewMCPServer(
"AI书签助手 🚀",
"1.0.0",
server.WithToolCapabilities(false),
)
// 添加获取URL建议工具
urlSuggestionTool := mcp.NewTool("get_url_suggestion",
mcp.WithDescription("获取URL的建议信息"),
mcp.WithString("url",
mcp.Required(),
mcp.Description("要分析的URL"),
),
)
s.AddTool(urlSuggestionTool, urlSuggestionHandler)
// 添加书签工具
addBookmarkTool := mcp.NewTool("add_bookmark",
mcp.WithDescription("添加新书签"),
mcp.WithString("url",
mcp.Required(),
mcp.Description("书签URL"),
),
mcp.WithString("category",
mcp.Description("书签分类"),
),
mcp.WithString("name",
mcp.Description("书签名称"),
),
mcp.WithString("description",
mcp.Description("书签描述"),
),
)
s.AddTool(addBookmarkTool, addBookmarkHandler)
// 启动Stdio服务器
if err := server.ServeStdio(s); err != nil {
log.Fatalf("服务器错误: %v\n", err)
}
},
}
func urlSuggestionHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
url, err := request.RequireString("url")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if !utils.IsValidURL(url) {
return mcp.NewToolResultError("无效的URL"), nil
}
// 获取配置
config := common.AppConfigModel
if config == nil {
return mcp.NewToolResultError("无法获取配置"), nil
}
webpageInfo, err := utils.GetWebpageInfo(url)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("获取网页信息失败: %v", err)), nil
}
// 获取数据库连接
db, err := utils.GetGormDB()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("获取数据库连接失败: %v", err)), nil
}
// 查询所有分类
var categories []models.Category
if err := db.Find(&categories).Error; err != nil {
return mcp.NewToolResultError(fmt.Sprintf("查询分类失败: %v", err)), nil
}
var categoryNames []string
for _, cat := range categories {
categoryNames = append(categoryNames, cat.Name)
}
// 初始化模型
maxTokens := config.AI.MaxTokens
temperature := float32(config.AI.Temperature)
model, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
BaseURL: config.AI.BaseURL,
APIKey: config.AI.PIKey,
Timeout: time.Duration(config.AI.Timeout) * time.Second,
Model: config.AI.Model,
MaxTokens: &maxTokens,
Temperature: &temperature,
})
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("初始化模型失败: %v", err)), nil
}
type Website struct {
Category string `json:"category"`
Name string `json:"name"`
Description string `json:"description"`
}
// 准备消息
messages := []*schema.Message{
{
Role: "system",
Content: `你是一个专业的书签助手,负责分析网页内容并生成合适的书签信息。
请遵循以下规则:
1. 分类选择:
- 分类名称应该简洁明了,尽量在10个字符以内,或者2-4个汉字
- 避免使用测试、测试1等明显不合适的分类
- 现有分类中没有合适的分类,优先创建新分类
2. 名称生成:
- 使用网页标题作为基础,但要去除网站名称、分隔符等无关信息
- 保持简洁,通常不超过10个汉字
- 如果标题不够清晰,可以根据内容补充关键信息
3. 描述生成:
- 总结网页的核心内容和价值
- 突出最重要的2-3个要点
- 使用简洁的语言,不超过50个汉字
- 避免使用"这是一个..."等冗余表达
- 使用markdown格式
请以JSON格式返回结果,格式如下:
{
"category": "分类名称",
"name": "书签名称",
"description": "书签描述"
}`,
},
{
Role: "user",
Content: fmt.Sprintf(`请分析以下网页内容并生成书签信息:
现有分类列表:%v
网页信息:
标题:%s
内容:%s
请确保:
1. 避免使用测试、测试1等明显不合适的分类
2. 如果已经有适合的分类,不要创建重复的分类,例如已经有ai,就不要创建人工智能等分类
3. 生成的名称要简洁明了
4. 描述要突出网页的核心价值`, categoryNames, webpageInfo.Title, webpageInfo.Text),
},
}
// 生成回复
response, err := model.Generate(ctx, messages)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("生成回复失败: %v", err)), nil
}
// 使用正则表达式去除 ```json 和 ```
re := regexp.MustCompile("(?s)^\\s*```json\\s*(.*?)\\s*```\\s*$")
matches := re.FindStringSubmatch(response.Content)
if len(matches) < 2 {
return mcp.NewToolResultError("无法提取JSON内容"), nil
}
cleanedJSON := matches[1]
// 解析JSON到结构体
var website Website
err = json.Unmarshal([]byte(cleanedJSON), &website)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("解析JSON时出错: %v", err)), nil
}
result := map[string]interface{}{
"category": website.Category,
"name": website.Name,
"description": website.Description,
}
resultJSON, err := json.Marshal(result)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("序列化结果失败: %v", err)), nil
}
return mcp.NewToolResultText(string(resultJSON)), nil
}
func addBookmarkHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
url, err := request.RequireString("url")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if !utils.IsValidURL(url) {
return mcp.NewToolResultError("无效的URL"), nil
}
category := request.GetString("category", "")
name := request.GetString("name", "")
description := request.GetString("description", "")
// 获取数据库连接
db, err := utils.GetGormDB()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("获取数据库连接失败: %v", err)), nil
}
// 如果没有提供名称,使用URL的域名
if name == "" {
name = utils.TruncateURLForName(url)
}
// 如果没有提供分类,使用"未分类"
if category == "" {
category = "未分类"
}
// 查询或创建分类
var categoryModel models.Category
if err := db.Where("name = ?", category).First(&categoryModel).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// 创建新分类
categoryModel = models.Category{
Name: category,
Description: category,
}
if err := db.Create(&categoryModel).Error; err != nil {
return mcp.NewToolResultError(fmt.Sprintf("创建分类失败: %v", err)), nil
}
} else {
return mcp.NewToolResultError(fmt.Sprintf("查询分类失败: %v", err)), nil
}
}
// 检查书签是否已存在
var existingBookmark models.Bookmark
if err := db.Where("title = ? OR url = ?", name, url).First(&existingBookmark).Error; err == nil {
return mcp.NewToolResultError("书签已存在"), nil
}
// 创建新书签
bookmark := models.Bookmark{
Title: name,
URL: url,
Description: description,
CategoryID: categoryModel.ID,
}
if err := db.Create(&bookmark).Error; err != nil {
return mcp.NewToolResultError(fmt.Sprintf("创建书签失败: %v", err)), nil
}
result := map[string]interface{}{
"message": fmt.Sprintf("书签 %s 添加成功", url),
"bookmark": map[string]interface{}{
"id": bookmark.ID,
"title": bookmark.Title,
"url": bookmark.URL,
"description": bookmark.Description,
"category": category,
},
}
resultJSON, err := json.Marshal(result)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("序列化结果失败: %v", err)), nil
}
return mcp.NewToolResultText(string(resultJSON)), nil
}
func init() {
rootCmd.AddCommand(mcpCmd)
}
这样,我们就对外暴露了两个工具:
可以让mcp客户端进行调用
3. MCP客户端配置 🔧
这里我们以cherry studio为例,介绍一下如何配置已经使用我们刚刚编写的mcp服务器。
由于这里涉及到打包,我们假定已经打包好了exe文件。
3.1 配置MCP
点击配置-MCP服务器-添加服务器
也可以编辑mcp配置文件
{
"mcpServers": {
"NCweVcwgsRqRDrRlrdq9R": {
"name": "AiBookmark",
"type": "stdio",
"description": "AiBookmark",
"isActive": true,
"command": "aibookmark.exe mcp",
"args": []
}
}
}
如果aibookmark
没有添加到系统环境变量中,需要指定完整路径。
3.2 使用MCP
在聊天框启用mcp,勾选上刚刚配置的mcp
接下来就可以像聊天一样添加书签了
往期系列

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。
更多推荐
所有评论(0)