本文以Go语言为例,介绍如何开发一个类似"MCP助手"的远程文件下载工具。该工具能够从指定URL下载文件并保存到本地,具备进度显示、断点续传等实用功能。

使用框架:github.com/mark3labs/mcp-go/server
MCP客户端:Cherry Studio


一、工具功能概述

核心功能点:

  • 通过 Tool 功能实现对目标链接文件保存到本地任意位置
  • 通过 Prompt 提供交互模板
  • 通过 Resource 展示本地资源

二、功能实现

搭建MCP服务器

声明MCP服务器, 有两种启动方式 Studio | SSE
对于Studio ,不需要端口信息,他的信息传入和输出都是走的进程管道标准输入和输出。

对于SSE,需要端口信息,他的信息传入和输出是基于HTTP服务提供的回话信息

我们通过MCP框架NewMCPServer创建一个MCP,声明了默认的日志输出行为和兜底逻辑。

	import "video-download-mcp/internal/server"
	....
	// Create the application MCP server instance
	srv := server.NewMCPServer()
	...
	case "stdio":
        log.Println("Starting MCP server with stdio transport...")
        go func() {
            if err := server.RunMCPServerWithStdio(srv); err != nil {
                log.Printf("MCP stdio server error: %v", err)
            }
        }()
    case "sse":
        log.Printf("Starting MCP server with SSE transport on port %d...", *port)
        go func() {
            if err := server.RunMCPServerWithSSE(srv, *port); err != nil {
                log.Printf("MCP SSE server error: %v", err)
            }
        }()
    default:
        log.Fatalf("unknown transport: %s (expected 'stdio' or 'sse')", *transport)
    }


// RunMCPServerWithStdio starts the MCP server using stdio transport
func RunMCPServerWithStdio(s *MCPServer) error {
	return mcpserver.ServeStdio(s.server)
}

// RunMCPServerWithSSE starts the MCP server using SSE transport on the given port
func RunMCPServerWithSSE(s *MCPServer, port int) error {
	sse := mcpserver.NewSSEServer(s.server)
	addr := fmt.Sprintf(":%d", port)
	return sse.Start(addr)
}


// NewMCPServer constructs a new MCP server and registers all tools
func NewMCPServer() *MCPServer {
	// Create MCP server with basic middlewares
	srv := mcpserver.NewMCPServer(
		"video-download",
		"1.0.0",
		mcpserver.WithLogging(),
		mcpserver.WithRecovery(),
	)

	return &MCPServer{server: srv}
}

通过MCP Tool功能实现对目标链接文件保存到本地任意位置

1、框架支持通过插件的方式声明MCPServer是否支持tool功能,我们通过NewTool方法,声明了我们服务其支持那种工具,他有以下三要素:

  • 工具的名称
  • 工具的描述
  • 工具的参数
// NewMCPServer constructs a new MCP server and registers all tools
func NewMCPServer() *MCPServer {
	....
	// Register tools
	tools.RegisterTools(srv)
	return &MCPServer{server: srv}
}

// RegisterTools registers all MCP tools for the service
func RegisterTools(s *mcpserver.MCPServer) {
	// Tool: download_video_file
	// Required params: url, save_dir, filename
	downloadTool := mcp.NewTool(
		"download_video_file",
		mcp.WithDescription("Download a video from URL and save to target directory with a given filename"),
		mcp.WithString("url", mcp.Description("The video file URL (HTTP/HTTPS)"), mcp.Required()),
		mcp.WithString("save_dir", mcp.Description("Directory where the video will be saved"), mcp.Required()),
		mcp.WithString("filename", mcp.Description("Target filename (without path)"), mcp.Required()),
	)
	s.AddTool(downloadTool, handleDownloadVideoFile)
}

2、实现远程文件保存功能

类似于Gin, McpServet 添加一个Tool和一个Tool的回调函数,当tool被调用时,McpServer会回调我们的 handleDownloadVideoFile 逻辑

// handleDownloadVideoFile executes the download and returns the absolute saved path
func handleDownloadVideoFile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	// Type-safe argument access – returns typed errors if wrong or missing
	url, err := req.RequireString("url")
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	saveDir, err := req.RequireString("save_dir")
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}
	fileName, err := req.RequireString("filename")
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	// Sanitize and compute target path
	targetPath := filepath.Join(saveDir, fileName)

	// Perform download via use case
	savedPath, err := usecase.DownloadVideo(ctx, url, targetPath)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to download video: %v", err)), nil
	}

	// Return absolute path
	return mcp.NewToolResultText(savedPath), nil
}

实现代码:
https://github.com/LaOzhOy1/video-download-mcp/commit/ac933e7ff767e0c968aeefde0388a5e6d3a8f0bf

在这里插入图片描述

通过Prompt能力,提供模板给用户输入

我们都知道,结构化的输入更有助于模型进行交互,调用mcp也不例外。
1、 声明一个Prompt对象,Tool的形式一样,它也有三要素:模板名称,模板描述,模板参数及其参数的解释
2、添加模板到MCP服务中
AddPrompt(p, func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error)
可以看到,这个方法有两个非常关键的点:

  • mcp.GetPromptRequest, 其中request.Params.Arguments藏有大模型提供给mcp的参数
  • mcp.NewPromptMessage,根据所给的参数,填入对应的模板中,返回一个系统的请求信息,告诉大模型要用什么工具去执行什么参数,这回带来一个好处,模型只要理解了Prompt的含义,就能根据我们预先规定的流程去执行逻辑,降低模型推理的不确定性(代理里只有一个download_video_file方法,如果有多个方法的调用顺序的话更明显)
// NewMCPServer constructs a new MCP server and registers all tools
func NewMCPServer() *MCPServer {
	....
	// Register tools
	tools.RegisterTools(srv)
	// Register prompts
	prompt.RegisterPrompts(srv)

	return &MCPServer{server: srv}
}
// registerPrompts defines MCP prompts that Cherry Studio can call to guide file downloads
func RegisterPrompts(s *mcpserver.MCPServer) {
    // Prompt: download_file_prompt – guides clients to call the tool with required args
    p := mcp.NewPrompt(
        "download_file_prompt",
        mcp.WithPromptDescription("Guide to download a file by URL to target directory with filename"),
        mcp.WithArgument("url", mcp.ArgumentDescription("The video/file URL (HTTP/HTTPS)"), mcp.RequiredArgument()),
        mcp.WithArgument("save_dir", mcp.ArgumentDescription("Destination directory to save the file"), mcp.RequiredArgument()),
        mcp.WithArgument("filename", mcp.ArgumentDescription("Target filename to use for saving"), mcp.RequiredArgument()),
    )

    s.AddPrompt(p, func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
        args := request.Params.Arguments
        url := fmt.Sprintf("%v", args["url"])      // simple extraction; validated by client
        dir := fmt.Sprintf("%v", args["save_dir"])  // required by prompt
        name := fmt.Sprintf("%v", args["filename"]) // required by prompt

        title := "Download File Instruction"
        // Compose assistant message instructing client to invoke the tool with these args
        msg := mcp.NewPromptMessage(
            mcp.RoleAssistant,
            mcp.NewTextContent(
                fmt.Sprintf(
                    "Use tool 'download_video_file' with arguments: url=%s, save_dir=%s, filename=%s. This will download the file to the destination and return the absolute path.",
                    url, dir, name,
                ),
            ),
        )

        return mcp.NewGetPromptResult(title, []mcp.PromptMessage{msg}), nil
    })
}

实现代码:
https://github.com/LaOzhOy1/video-download-mcp/commit/4223a91bf549eec4855788cbfaef893a4385eb5b
在这里插入图片描述

通过Resource展示已下载资源

服务器运行过程中下载的资源保存在本地,这里我们用简单的一个JSON文件作为工程的数据记录,在工程启动时加载数据文件,并在运行时维护这份数据

var (
	mu        sync.RWMutex
	downloads []string
	dbPath    = filepath.Join(".", "downloads_db.json")
)

func init() {
	_ = load()
}

func RecordDownload(path string) error {
	mu.Lock()
	defer mu.Unlock()
	// avoid duplicates
	for _, p := range downloads {
		if p == path {
			return nil
		}
	}
	downloads = append(downloads, path)
	return save()
}

func ListDownloads() []string {
	mu.RLock()
	defer mu.RUnlock()
	out := make([]string, len(downloads))
	copy(out, downloads)
	return out
}

每当大模型调用保存文件的功能时,调用RecordDownload方法记录数据,并返回保存结果。

// handleDownloadVideoFile executes the download and returns the absolute saved path
func handleDownloadVideoFile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  ...
	// Perform download via use case (with progress callback placeholder)
	savedPath, derr := usecase.DownloadVideoWithProgress(ctx, url, targetPath, func(written int64, total int64) {})
	...
	// Record successfully downloaded path
	_ = storage.RecordDownload(savedPath)

	// Return absolute path
	return mcp.NewToolResultText(savedPath), nil
}

与Prompt和Tool一样,Resource也是MCP的原生支持的功能,我们需要告诉MCP客户端,服务端支持Resource能力。我们提供 downloads://list 接口给客户端调用,查询下载的结果,返回的类型时Json格式。

// RegisterResources registers server resources for clients to read
func RegisterResources(s *mcpserver.MCPServer) {
    // Resource: list of downloaded file paths
    res := mcp.NewResource(
        "downloads://list",
        "Downloaded Files",
        mcp.WithResourceDescription("List of all downloaded files' absolute paths"),
        mcp.WithMIMEType("application/json"),
    )

    s.AddResource(res, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
        paths := storage.ListDownloads()
        data, err := json.Marshal(paths)
        if err != nil {
            return nil, err
        }
        return []mcp.ResourceContents{
            mcp.TextResourceContents{
                URI:      "downloads://list",
                MIMEType: "application/json",
                Text:     string(data),
            },
        }, nil
    })
}

在这里插入图片描述

三、成果展示

在代码里我们实现一个Tool工具方法,提供给大模型调用保存视频到本地,调用这个工具方法之前,模型会根据Prompt 给与我们调用的入参提示,但是很遗憾,Cherry Studio 没有给Resource做交互界面,所以我们展示文件下载保存的流程:

1、输入内容告诉大模型我们要下载的视频链接和保存路径

帮我保存这个视频
视频链接: xxxx
保存文件名称: 测试视频
保存的文件路径:D:\资源爬取

2、 大模型接受用户的信息之前,cherry studio会对用户消息进行加工,告诉大模型一个Prompt,关于如何下载一个网络文件到本地的调用信息,还告诉大模型有一个MCP服务器Tool工具可用,这部分用户是不可见的,是内嵌在我们的Cherry studio当中的调用流程。
3、大模型解析加工后的消息数据后,会返回给Cherry Studio,告诉他要用什么工具进行下载以及调用这些工具所需要的参数,最终让Cherry Studio 调用对应方法进行视频下载。

结果:
在这里插入图片描述

大致流程图:
在这里插入图片描述

思考

MCP可以被HTTP接口取代吗

在详尽的为大模型描述下,HTTP接口也可以做到MCP类似的功能,但是MCP是一种大家斗公认的规范,是在HTTP接口上做了交互的约束,是一种针对大模型交互的标准,只有市面上的所有的MCP客户端遵守标准,才能让MCP更加适用。

多个MCP如何进行协调

每个MCP工具都有他自己的描述,大模型只会回答你提问的问题,MCP工具的调用,更多依赖于Client端的编排逻辑,比如:多轮对话循环,直到大模型不在进行工具调用后,再将最终的结果总结返回给用户,期间的MCP调用可以是并行的也可以是串行的,一句话而言:决策的东西让AI大模型做,工具编排的流程让Cilent来做。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐