一、从 function calling 开始

要想理解 MCP,需要先理解 function calling 的概念

1.1 function calling 要解决什么问题?

大模型目前存在比较明显的两个问题

  • 数据非最新,大模型训练成本很大,所以训练好了的数据无法做到与时俱进。
  • 数据孤岛 - 数据领域受限,大模型训练的数据大多是公开的信息,私有领域的数据无法获取,所以很需要用大模型专门针对特定领域内的知识库进行训练。无法做到数据共享
  • 无法调用外部能力,大模型只能对文本输出进行概率的计算,但没有真正的逻辑能力,也无法调用外部服务进行更多的任务执行。

1.2 function calling 是什么?

function calling 的出现就是为了解决数据孤岛和调用外部能力的问题,通过 AI client 主导告诉大模型可以调用哪些 tools,进而增强的大模型使用工具的能力。

1.3 function calling 如何运作?

通过上图,在调用大模型之前,AI client 会告诉大模型可以调用哪些 tools,大模型在处理第一轮提问时,会判断是否需要调用哪些 function call, client 会在调用 tools 后,再进行第二轮提问。最终大模型会返回调用的 tools 的响应。

代码示例( [openai 官网示例](https://platform.openai.com/docs/guides/function-calling?api-mode=responses&lang=javascript):

import { OpenAI } from "openai";

const openai = new OpenAI();

// client 在调用大模型之前,需要告诉 tools 有哪些,tools 可以干什么, 以及什么时候可以调用 tools
const tools = [{
    type: "function",
    name: "get_weather",
    description: "Get current temperature for provided coordinates in celsius.",
    parameters: {
        type: "object",
        properties: {
            latitude: { type: "number" },
            longitude: { type: "number" }
        },
        required: ["latitude", "longitude"],
        additionalProperties: false
    },
    strict: true
}];

const input = [
    {
        role: "user",
        content: "What's the weather like in Paris today?"
    }
];

// 第一轮对话,client 需要主动告诉大模型有哪些tools,以及可以在什么情况下调用哪些 tools
// 大模型会自主决定是否需要调用 tools
const response = await openai.responses.create({
    model: "gpt-4o",
    input,
    tools,
});
/**
*
* [{
*    "type": "function_call",
*    "id": "fc_12345xyz",
*    "call_id": "call_12345xyz",
*    "name": "get_weather",
*    "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
*  }]
*/

// 大模型会自主决定是否需要调用 tools,以及调用哪些 tools
// 如果需要调用 tools 会返回 type = "function_call" 的响应
for (const toolCall of response.output) {
    if (toolCall.type !== "function_call") {
        continue;
    }
    const name = toolCall.name;
    const args = JSON.parse(toolCall.arguments);

    const result = callFunction(name, args);
    input.push({
        type: "function_call_output",
        call_id: toolCall.call_id,
        output: result.toString()
    });
}

// client 在第二轮对话时,需要告诉大模型调用了什么 tools(通过 call_id) 识别
input.push(toolCall); 
input.push({                              
    type: "function_call_output",
    call_id: toolCall.call_id,
    output: result.toString()
});


// 大模型在第二轮对话会返回基于 function_call 给予的信息推理出来的响应
const response2 = await openai.responses.create({
    model: "gpt-4o",
    input,
    tools,
    store: true,
});

console.log(response2.output_text)


const callFunction = async (name, args) => {
    if (name === "get_weather") {
        return getWeather(args.latitude, args.longitude);
    }
    if (name === "send_email") {
        return sendEmail(args.to, args.body);
    }
};

// tooling 提供了调用外部 API 查询信息的能力
async function getWeather(latitude, longitude) {
    const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m`);
    const data = await response.json();
    return data.current.temperature_2m;
}

1.4 function calling 的优势特点

  1. 结构化参数,大模型会严格按照数据格式协议调用 tools
  2. 意图识别:大模型会自主识别是否需要调用 tools

1.5 function calling 的问题

  1. tools 必须和 client 调用关联耦合在一起
  2. 可拓展性差,只能在 web 版的 chatGPT 上使用,需要审核而且有使用限制
  3. 只支持 HTTP 协议

二、chatGPT Actions

https://platform.openai.com/docs/actions/getting-started

基于 function call, chatGPT 支持在高级版中声明 function calling 协议,从而扩展了 chatGPT 调用第三方接口的能力。

参考官网的示例,需要在 chatGPT 中配置第三方接口调用的 yaml 格式 schema


openapi: 3.1.0
info:
  title: NWS Weather API
  description: Access to weather data including forecasts, alerts, and observations.
  version: 1.0.0
servers:
  - url: https://api.weather.gov
    description: Main API Server
paths:
  /points/{latitude},{longitude}:
    get:
      operationId: getPointData
      summary: Get forecast grid endpoints for a specific location
      parameters:
        - name: latitude
          in: path
          required: true
          schema:
            type: number
            format: float
          description: Latitude of the point
        - name: longitude
          in: path
          required: true
          schema:
            type: number
            format: float
          description: Longitude of the point
      responses:
        '200':
          description: Successfully retrieved grid endpoints
          content:
            application/json:
              schema:
                type: object
                properties:
                  properties:
                    type: object
                    properties:
                      forecast:
                        type: string
                        format: uri
                      forecastHourly:
                        type: string
                        format: uri
                      forecastGridData:
                        type: string
                        format: uri

  /gridpoints/{office}/{gridX},{gridY}/forecast:
    get:
      operationId: getGridpointForecast
      summary: Get forecast for a given grid point
      parameters:
        - name: office
          in: path
          required: true
          schema:
            type: string
          description: Weather Forecast Office ID
        - name: gridX
          in: path
          required: true
          schema:
            type: integer
          description: X coordinate of the grid
        - name: gridY
          in: path
          required: true
          schema:
            type: integer
          description: Y coordinate of the grid
      responses:
        '200':
          description: Successfully retrieved gridpoint forecast
          content:
            application/json:
              schema:
                type: object
                properties:
                  properties:
                    type: object
                    properties:
                      periods:
                        type: array
                        items:
                          type: object
                          properties:
                            number:
                              type: integer
                            name:
                              type: string
                            startTime:
                              type: string
                              format: date-time
                            endTime:
                              type: string
                              format: date-time
                            temperature:
                              type: integer
                            temperatureUnit:
                              type: string
                            windSpeed:
                              type: string
                            windDirection:
                              type: string
                            icon:
                              type: string
                              format: uri
                            shortForecast:
                              type: string
                            detailedForecast:
                              type: string

三、什么是 MCP?

和 function calling 一样的目的,为了让大模型更好和第三方 API, 私有数据库交互,claudAI 开源了 MCP(Model Context Protocol),

如何实现一个 mcp server

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

// Create server instance
const server = new McpServer({
  name: "weather",
  version: "1.0.0",
  capabilities: {
    resources: {},
    tools: {},
  },
});

// Helper function for making NWS API requests
async function makeNWSRequest<T>(url: string): Promise<T | null> {
  const headers = {
    "User-Agent": USER_AGENT,
    Accept: "application/geo+json",
  };

  try {
    const response = await fetch(url, { headers });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return (await response.json()) as T;
  } catch (error) {
    console.error("Error making NWS request:", error);
    return null;
  }
}

interface AlertFeature {
  properties: {
    event?: string;
    areaDesc?: string;
    severity?: string;
    status?: string;
    headline?: string;
  };
}

// Format alert data
function formatAlert(feature: AlertFeature): string {
  const props = feature.properties;
  return [
    `Event: ${props.event || "Unknown"}`,
    `Area: ${props.areaDesc || "Unknown"}`,
    `Severity: ${props.severity || "Unknown"}`,
    `Status: ${props.status || "Unknown"}`,
    `Headline: ${props.headline || "No headline"}`,
    "---",
  ].join("\n");
}

interface ForecastPeriod {
  name?: string;
  temperature?: number;
  temperatureUnit?: string;
  windSpeed?: string;
  windDirection?: string;
  shortForecast?: string;
}

interface AlertsResponse {
  features: AlertFeature[];
}

interface PointsResponse {
  properties: {
    forecast?: string;
  };
}

interface ForecastResponse {
  properties: {
    periods: ForecastPeriod[];
  };
}


// server 注册有哪些 tool
server.tool(
  "get-alerts",
  "Get weather alerts for a state",
  {
    state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"),
  },
  async ({ state }) => {
    const stateCode = state.toUpperCase();
    const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
    const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);

    if (!alertsData) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to retrieve alerts data",
          },
        ],
      };
    }

    const features = alertsData.features || [];
    if (features.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: `No active alerts for ${stateCode}`,
          },
        ],
      };
    }

    const formattedAlerts = features.map(formatAlert);
    const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;

    return {
      content: [
        {
          type: "text",
          text: alertsText,
        },
      ],
    };
  },
);

server.tool(
  "get-forecast",
  "Get weather forecast for a location",
  {
    latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
    longitude: z.number().min(-180).max(180).describe("Longitude of the location"),
  },
  async ({ latitude, longitude }) => {
    // Get grid point data
    const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
    const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);

    if (!pointsData) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
          },
        ],
      };
    }

    const forecastUrl = pointsData.properties?.forecast;
    if (!forecastUrl) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to get forecast URL from grid point data",
          },
        ],
      };
    }

    // Get forecast data
    const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
    if (!forecastData) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to retrieve forecast data",
          },
        ],
      };
    }

    const periods = forecastData.properties?.periods || [];
    if (periods.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: "No forecast periods available",
          },
        ],
      };
    }

    // Format forecast periods
    const formattedForecast = periods.map((period: ForecastPeriod) =>
      [
        `${period.name || "Unknown"}:`,
        `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
        `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
        `${period.shortForecast || "No forecast available"}`,
        "---",
      ].join("\n"),
    );

    const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;

    return {
      content: [
        {
          type: "text",
          text: forecastText,
        },
      ],
    };
  },
);

MCP 的优势

  • 开放,协议开源,任何 AI client 都可以接入 MCP client 和 server

MCP 相关资源

  1. MCP Server 开放服务列表:

2. 【Introducing the Model Context Protocol】https://www.anthropic.com/news/model-context-protocol

3. 【Model Context Protocol Guides】https://docs.anthropic.com/en/docs/agents-and-tools/mcp

4. 【Model Context Protocol GetStart】https://modelcontextprotocol.io/introduction

5. 【MCP Quickstart Resource】https://github.com/modelcontextprotocol/quickstart-resources#

5. 【MCP Typescript SDK】https://github.com/modelcontextprotocol/typescript-sdk/tree/main

6. 【OpenAI Function Call】https://platform.openai.com/docs/guides/function-calling?api-mode=responses