Published on

Claude MCP 使用及搭建

Authors
  • avatar
    Name
    Shoukai Huang
    Twitter

近期使用 Windsurf 开发工具进行代码编写,惊叹于开发效率的同时,对其如何使用的本地文件方式比较疑惑,看到 MCP 协议后,解答了个人疑惑。MCP 通过一套标准化协议,将 AI 应用和数据源连接起来。

总结概念的同时,动手尝试了一下,当然也遇到了一些问题,环境问题居多,估计过段时间这些问题都不再是问题。

1. 基础工具

uv 是一个由 Rust 编写的极快的 Python 包和项目管理工具,由 Astra 支持。

安装(Install uv with our standalone installers)

# On macOS and Linux.
curl -LsSf https://astral.sh/uv/install.sh | sh
# On Windows.
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

(brew 安装要求 macos 版本,如果受限,可以使用 curl 安装)

2. 理论整理

2.1 核心概念

Model Context Protocol (MCP)

模型上下文协议 (MCP),这是一种将 AI 助手连接到数据所在的系统(包括内容存储库、业务工具和开发环境)的新标准。其目的是帮助 Frontier 模型产生更好、更相关的响应。

Model Context Protocol (MCP), a new standard for connecting AI assistants to the systems where data lives, including content repositories, business tools, and development environments. Its aim is to help frontier models produce better, more relevant responses.

目前模型受到数据隔离的限制,它们被困在信息孤岛和遗留系统后面。每个新数据源都需要自己的自定义实施,这使得真正连接的系统难以扩展。MCP 解决了这一挑战。它提供了一个通用的开放标准,用于将 AI 系统与数据源连接起来,用单一协议取代碎片化的集成。结果是一种更简单、更可靠的方法,使 AI 系统能够访问所需的数据。

Model Context Protocol 是一种开放标准,使开发人员能够在其数据源和 AI 驱动的工具之间构建安全的双向连接。架构很简单:开发人员可以通过 MCP 服务器公开他们的数据,也可以构建连接到这些服务器的 AI 应用程序(MCP 客户端)。

三个主要组件:

  • 模型上下文协议规范和 SDK;
  • Claude Desktop 应用程序中的本地 MCP 服务器支持;
  • MCP 服务器的开源存储库;

MCP 的核心遵循客户端-服务器架构,其中主机应用程序可以连接到多个服务器

(来源:https://modelcontextprotocol.io/introduction )

(来源:https://modelcontextprotocol.io/introduction

图中各个部分:

  • MCP Hosts:希望通过 MCP 访问数据的 Claude Desktop、IDE 或 AI 工具等程序 (人机交互或者系统交互提供端,AI 能力提供者,例如:Claude Desktop)
  • MCP Clients:与服务器保持 1:1 连接的协议客户端;(MCP 通讯的 client 端,Claude Desktop 包含该功能,不用单独安装)
  • MCP Servers:轻量级程序,每个程序都通过标准化的模型上下文协议公开特定功能;(npx 或者 uvx 启动的 Server,Server 需要单独安装和下载)
  • Remote Services:MCP 服务器可以连接到的互联网(例如,通过 API)提供的外部系统(外部系统)

2.2 实现原理

MCP 基础协议

MCP 协议遵循客户端-主机-服务器架构,MCP 协议其实就是规定的组件之间的通信协议,而 MCP 中的所有消息必须遵循 JSON-RPC 2.0 规范。

消息类型,MCP 协议定义了三种类型的消息:

  • request:请求消息,用于客户端向服务器发送请求,也可以从服务器发送到客户端
  • response:响应消息,用于对请求的响应。
  • notification:通知消息,用于服务器向客户端发送通知。

通过 $ npx @modelcontextprotocol/create-server weather-server 搭建 server 系统后,直观的看到协议实现内容。

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

查找 @modelcontextprotocol 包内容,可以看到里面 server、client 及 SDK 内容。

image.png

SDK 里面即为协议定义,MCP 通过 Schema 使模型(AI)理解工具定义和如何调用,进而实现上下文交互。

3. 实践操作

本文安装环境:

  • macos 12 (brew uv 存在问题,通过 curl 方式安装 uv)
  • uv(python 3.12.0)
  • git
  • npx (node v18.19.0)

node 和 python 存在版本要求,注意设置环境变量和升级。本文 python 采用 pyenv 方式进行版本控制,node 采用 nvm 方式进行版本控制。

Claude Desktop 配置文件 claude_desktop_config.json 如下:

{
  "mcpServers": {
    "sqlite": {
      "command": "/Users/{USER_NAME}/.local/bin/uvx",
      "args": ["mcp-server-sqlite","--python /Users/{USER_NAME}/.pyenv/versions/3.12.0/bin/python3.12", "--db-path", "/Users/{USER_NAME}/test.db"]
    },
    "filesystem": {
      "command": "/Users/{USER_NAME}/.nvm/versions/node/v18.19.0/bin/node",
      "args": ["/Users/{USER_NAME}/.nvm/versions/node/v18.19.0/lib/node_modules/@modelcontextprotocol/server-filesystem/dist/index.js", "/{FILE_PATH}"]
    }
  }
}

如果有问题,及时查看日志,mcp.log 同级目录存在其他 server 的日志。

image.png

注意:当前版本的 Claude Desktop 对于环境变量不是很友好,zsh 验证可以,但是 Claude Desktop 仍然报错,需要指定全路径。

image.png

输入框右下角显示工具图标即可使用本地 MCP 工具。

问题:由于环境问题,uv 命令一直未成功,一直使用 python 3.9 版本,然后报兼容性问题。zsh 使用的 3.12 版本,也未找到 3.9 版本的环境变量,使用 node 18 版本,可以完成验证过程。

4. 扩展探索

按照:编写一个 TypeScript MCP 服务器,进行进行代码编写,详细操作过程见原文即可。附带源码的原因是当前版本原文存在字符集问题,已经修正。

src/types/weather.ts

点击展开/收起代码
// src/types/weather.ts
export interface OpenWeatherResponse {
    main: {
        temp: number;
        humidity: number;
    };
    weather: Array<{
        description: string;
    }>;
    wind: {
        speed: number;
    };
    dt_txt?: string;
}

export interface WeatherData {
    temperature: number;
    conditions: string;
    humidity: number;
    wind_speed: number;
    timestamp: string;
}

export interface ForecastDay {
    date: string;
    temperature: number;
    conditions: string;
}

export interface GetForecastArgs {
    city: string;
    days?: number;
}

// 类型保护函数,用于检查 GetForecastArgs 类型
export function isValidForecastArgs(args: any): args is GetForecastArgs {
    return (
        typeof args === "object" &&
        args !== null &&
        "city" in args &&
        typeof args.city === "string" &&
        (args.days === undefined || typeof args.days === "number")
    );
}

src/index.ts

点击展开/收起代码
#!/usr/bin/env node

/**
 * This is a template MCP server that implements a simple notes system.
 * It demonstrates core MCP concepts like resources and tools by allowing:
 * - Listing notes as resources
 * - Reading individual notes
 * - Creating new notes via a tool
 * - Summarizing all notes via a prompt
 */

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ErrorCode,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import dotenv from "dotenv";
import {
  WeatherData,
  ForecastDay,
  OpenWeatherResponse,
  isValidForecastArgs,
} from "./types/weather.js";

dotenv.config();

const API_KEY = process.env.OPENWEATHER_API_KEY;
if (!API_KEY) {
  throw new Error("OPENWEATHER_API_KEY environment variable is required");
}

const API_CONFIG = {
  BASE_URL: "http://api.openweathermap.org/data/2.5",
  DEFAULT_CITY: "San Francisco",
  ENDPOINTS: {
    CURRENT: "weather",
    FORECAST: "forecast",
  },
} as const;

class WeatherServer {
  private server: Server;
  private axiosInstance;

  constructor() {
    this.server = new Server(
      {
        name: "weather-server",
        version: "0.1.0",
      },
      {
        capabilities: {
          resources: {},
          tools: {},
        },
      }
    );

    // 配置 axios 实例
    this.axiosInstance = axios.create({
      baseURL: API_CONFIG.BASE_URL,
      params: {
        appid: API_KEY,
        units: "metric",
      },
    });

    this.setupHandlers();
    this.setupErrorHandling();
  }

  private setupErrorHandling(): void {
    this.server.onerror = (error) => {
      console.error("[MCP Error]", error);
    };

    process.on("SIGINT", async () => {
      await this.server.close();
      process.exit(0);
    });
  }

  private setupHandlers(): void {
    this.setupResourceHandlers();
    this.setupToolHandlers();
  }

  private setupResourceHandlers(): void {
    // TODO: 实现资源处理器
    this.server.setRequestHandler(
      ListResourcesRequestSchema,
      async () => ({
        resources: [{
          uri: `weather://${API_CONFIG.DEFAULT_CITY}/current`,
          name: `Current weather in ${API_CONFIG.DEFAULT_CITY}`,
          mimeType: "application/json",
          description: "Real-time weather data including temperature, conditions, humidity, and wind speed"
        }]
      })
    );

    this.server.setRequestHandler(
      ReadResourceRequestSchema,
      async (request) => {
        const city = API_CONFIG.DEFAULT_CITY;
        if (request.params.uri !== `weather://${city}/current`) {
          throw new McpError(
            ErrorCode.InvalidRequest,
            `Unknown resource: ${request.params.uri}`
          );
        }

        try {
          const response = await this.axiosInstance.get<OpenWeatherResponse>(
            API_CONFIG.ENDPOINTS.CURRENT,
            {
              params: { q: city }
            }
          );

          const weatherData: WeatherData = {
            temperature: response.data.main.temp,
            conditions: response.data.weather[0].description,
            humidity: response.data.main.humidity,
            wind_speed: response.data.wind.speed,
            timestamp: new Date().toISOString()
          };

          return {
            contents: [{
              uri: request.params.uri,
              mimeType: "application/json",
              text: JSON.stringify(weatherData, null, 2)
            }]
          };
        } catch (error) {
          if (axios.isAxiosError(error)) {
            throw new McpError(
              ErrorCode.InternalError,
              `Weather API error: ${error.response?.data.message ?? error.message}`
            );
          }
          throw error;
        }
      }
    );
  }

  private setupToolHandlers(): void {
    // TODO: 实现工具处理器
    this.server.setRequestHandler(
      ListToolsRequestSchema,
      async () => ({
        tools: [{
          name: "get_forecast",
          description: "Get weather forecast for a city",
          inputSchema: {
            type: "object",
            properties: {
              city: {
                type: "string",
                description: "City name"
              },
              days: {
                type: "number",
                description: "Number of days (1-5)",
                minimum: 1,
                maximum: 5
              }
            },
            required: ["city"]
          }
        }]
      })
    );

    this.server.setRequestHandler(
      CallToolRequestSchema, async (request) => {
        if (request.params.name !== "get_forecast") {
          throw new McpError(
            ErrorCode.MethodNotFound,
            `Unknown tool: ${request.params.name}`
          );
        }

        if (!isValidForecastArgs(request.params.arguments)) {
          throw new McpError(
            ErrorCode.InvalidParams,
            "Invalid forecast arguments"
          );
        }

        const city = request.params.arguments.city;
        const days = Math.min(request.params.arguments.days || 3, 5);

        try {
          const response = await this.axiosInstance.get<{
            list: OpenWeatherResponse[]
          }>(API_CONFIG.ENDPOINTS.FORECAST, {
            params: {
              q: city,
              cnt: days * 8 // API 返回 3 小时间隔的数据
            }
          });

          const forecasts: ForecastDay[] = [];
          for (let i = 0; i < response.data.list.length; i += 8) {
            const dayData = response.data.list[i];
            forecasts.push({
              date: dayData.dt_txt?.split(' ')[0] ?? new Date().toISOString().split('T')[0],
              temperature: dayData.main.temp,
              conditions: dayData.weather[0].description
            });
          }

          return {
            content: [{
              type: "text",
              text: JSON.stringify(forecasts, null, 2)
            }]
          };
        } catch (error) {
          if (axios.isAxiosError(error)) {
            return {
              content: [{
                type: "text",
                text: `Weather API error: ${error.response?.data.message ?? error.message}`
              }],
              isError: true,
            }
          }
          throw error;
        }
      }
    );
  }

  async run(): Promise<void> {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);

    console.error("Weather MCP server running on stdio");
  }
}

const server = new WeatherServer();
server.run().catch(console.error);

(全部代码来源于:www.claudemcp.com ,部分乱码修正 by kimi,字符转义问题)

image.png

修改配置,部署到 Claude Desktop ,OPENWEATHER_API_KEY 替换为申请得到的 KEY

{
  "mcpServers": {
    "sqlite": {
      "command": "/Users/{USER_NAME}/.local/bin/uvx",
      "args": ["mcp-server-sqlite","--python /Users/{USER_NAME}/.pyenv/versions/3.12.0/bin/python3.12", "--db-path", "/Users/{USER_NAME}/test.db"]
    },
    "filesystem": {
      "command": "/Users/{USER_NAME}/.nvm/versions/node/v18.19.0/bin/node",
      "args": ["/Users/{USER_NAME}/.nvm/versions/node/v18.19.0/lib/node_modules/@modelcontextprotocol/server-filesystem/dist/index.js", "/{FILE_PATH}"]
    },
    "weather-server": {
      "command": "/Users/{USER_NAME}/.nvm/versions/node/v18.19.0/bin/node",
      "args": [
        "/Users/{USER_NAME}/source/source-node/weather-server/build/index.js"
      ],
      "env": {
        "OPENWEATHER_API_KEY": "{OPENWEATHER_API_KEY}"
      }
    }
  }
}

询问 Claude 未来 5 天的天气预报:

Can you get me a 5-day forecast for Beijing and tell me if I should pack an umbrella?

image.png

会调用工具显示结果。

image.png

点击 weather-server 里面可以得到过程数据

image.png

数据内容(当前发版日期为:2024 年 12 月 16 日)为

{
  `city`: `Beijing`,
  `days`: 5
}
[
  {
    "date": "2024-12-16",
    "temperature": 5.94,
    "conditions": "clear sky"
  },
  {
    "date": "2024-12-17",
    "temperature": 1.98,
    "conditions": "clear sky"
  },
  {
    "date": "2024-12-18",
    "temperature": 0.81,
    "conditions": "clear sky"
  },
  {
    "date": "2024-12-19",
    "temperature": 0.69,
    "conditions": "overcast clouds"
  },
  {
    "date": "2024-12-20",
    "temperature": 0.55,
    "conditions": "clear sky"
  }
]

更多 server 代码及实现过程可以通过: https://github.com/modelcontextprotocol/servers 获取。

5. 资源获取

本文资料来源