AI交互适配协议

Model Context Protocol (MCP) 是一种通用适配器协议,用于连接AI应用与外部系统和数据,通过Google Apps Script实现MCP服务器,使AI能够安全访问和利用Google Workspace服务中的数据。

  • by Macorreag
  • 本地部署
  • 更新于
在 GitHub 查看 macorreag/mcpapp

使用说明

MCPApp

概述

本文介绍了用于AI交互的模型上下文协议(MCP),探索了作为服务器选项的Google Apps脚本(GAS)。它通过示例展示了可行性,但指出缺乏GAS SDK,旨在鼓励理解和开发。

描述

模型上下文协议(MCP)是一种新兴的标准,作为将AI应用程序与外部系统和数据连接的通用适配器。本文探讨了使用Google Apps Script实现MCP服务器的可行性。这样做可以使人工智能通过标准化协议安全地访问和利用谷歌工作区服务中的数据,如文档、表格和日历,从而促进人工智能驱动的工作流程。尽管缺少官方的Google Apps script SDK,但示例脚本展示了这种潜力。虽然存在平台限制,但这种方法对于内部或以用户为中心的应用程序可能很有价值,可能会鼓励专门的SDK开发,以实现更深层次的AI Google Workspace集成。

用法

1.创建Google Apps脚本项目

请创建独立类型的Google Apps脚本项目。 参考 当然,此报告中的示例脚本也可用于容器绑定脚本类型。

打开已创建的Google Apps脚本项目的脚本编辑器。

2.安装库

仓库

https://github.com/tanaikech/MCPApp

图书馆项目密钥

1TlX_L9COAriBlAYvrMLiRFQ5WVf1n0jChB6zHamq2TNwuSbVlI5sBUzh

为了简单地部署MCP服务器,我将脚本创建为Google Apps脚本库。要使用此库,请按如下方式安装库。

  1. 创建GAS项目:您可以将此库用于独立类型和容器绑定脚本类型的GAS项目。
  2. 安装此库:图书馆的项目密钥是 1TlX_L9COAriBlAYvrMLiRFQ5WVf1n0jChB6zHamq2TNwuSbVlI5sBUzh.

3.脚本

您可以在我的存储库中看到整个脚本,包括库。 https://github.com/tanaikech/MCPApp

示例脚本如下:请将以下脚本复制粘贴到脚本编辑器中并保存。 MCPApp 是已安装库的标识符。

在这个示例脚本中,实现了“工具”、“资源”和“提示”。在“工具”和“资源”中,使用函数从Google云端硬盘和Google日历创建响应值,并将其返回给MCP客户端。

/**
 * This function is automatically run when the MCP client accesses Web Apps.
 */
function doPost(eventObject) {
  const object = {
    eventObject,
    serverResponse: getserverResponse_(),
    functions: getFunctions_(),
  };
  return new MCPApp.mcpApp({ accessKey: "sample" }).server(object);
}

作用 getserverResponse_() 具体如下。请查看评论。

/**
 * Please set and modify the following JSON to your situation.
 * The key is the method from the MCP client.
 * The value is the object for returning to the MCP client.
 * ID is automatically set in the script.
 * The specification of this can be seen in the official document.
 * Ref: https://modelcontextprotocol.io/specification/2025-03-26
 */
function getserverResponse_() {
  return {
    /**
     * Response to "initialize"
     */
    initialize: {
      jsonrpc: "2.0",
      result: {
        protocolVersion: "2024-11-05", // or "2025-03-26"
        capabilities: {
          experimental: {},
          prompts: {
            listChanged: false,
          },
          resources: {
            subscribe: false,
            listChanged: false,
          },
          tools: {
            listChanged: false,
          },
        },
        serverInfo: {
          name: "sample server from MCPApp",
          version: "1.0.0",
        },
      },
    },

    /**
     * Response to "tools/list"
     */
    "tools/list": {
      jsonrpc: "2.0",
      result: {
        tools: [
          {
            name: "search_files_on_Google_Drive", // <--- It is required to create a function of the same name as this.
            description: "Search files on Google Drive.",
            inputSchema: {
              type: "object",
              properties: {
                folderName: {
                  description:
                    "Search files in the folder of this folder name.",
                  type: "string",
                },
              },
              required: ["folderName"],
            },
          },
          {
            name: "search_schedule_on_Google_Calendar", // <--- It is required to create a function of the same name as this.
            description: "Search the schedule on Google Calendar.",
            inputSchema: {
              type: "object",
              properties: {
                date: {
                  description:
                    "Search the schedule on Google Calendar by giving the date.",
                  type: "string",
                  format: "date",
                },
              },
              required: ["date"],
            },
          },
        ],
      },
    },

    /**
     * Response to "resources/list"
     */
    "resources/list": {
      jsonrpc: "2.0",
      result: {
        resources: [
          {
            uri: "get_today_schedule", // <--- It is required to create a function of the same name as this.
            name: "today_schedule",
            description: "Today's schedule for Tanaike.",
            mimeType: "text/plain",
          },
        ],
        nextCursor: "next-page-cursor",
      },
    },

    /**
     * Response to "prompts/list"
     */
    "prompts/list": {
      jsonrpc: "2.0",
      result: {
        prompts: [
          {
            name: "search_files_from_Google_Drive",
            description:
              "Search files in the specific folder on Google Drive using tools.",
            arguments: [
              {
                name: "search_files",
                description: "Search files.",
                required: true,
              },
            ],
          },
        ],
        nextCursor: "next-page-cursor",
      },
    },

    /**
     * Response to "prompts/get"
     */
    "prompts/get": {
      jsonrpc: "2.0",
      result: {
        description: "Search files in the specific folder on Google Drive.",
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: "Return file information from a folder of 'sample' on Google Drive.",
            },
          },
        ],
      },
    },
  };
}

作用 getFunctions_() 具体如下。请查看评论。这些函数在MCP客户端调用时运行 tools/call 和 resources/read.

/**
 * "tools/call": The function name is required to be the same as the name declared at "tools/list".
 * "resources/read": The function name is required to be the same as the uri declared at "resources/list".
 */
function getFunctions_() {
  return {
    "tools/call": {
      search_files_on_Google_Drive,
      search_schedule_on_Google_Calendar,
    },
    "resources/read": { get_today_schedule },
  };
}

在这里, search_files_on_Google_Drive, search_schedule_on_Google_Calendar,以及 get_today_schedule 是功能。函数名必须与在“tools/list”中声明的名称和在“resources/list”处声明的uri相同。此外,返回值的格式需要遵循MCP的规范。这是此脚本的规范。

功能如下。

/**
 * This function retrieves the file metadata from the specific folder on Google Drive.
 *
 * This function is run by "tools/call".
 * "tools/call": The function name is required to be the same as the name declared at "tools/list".
 */
function search_files_on_Google_Drive(args) {
  const { folderName } = args;
  try {
    const res = [];
    const folders = DriveApp.getFoldersByName(folderName);
    while (folders.hasNext()) {
      const folder = folders.next();
      const files = folder.getFiles();
      while (files.hasNext()) {
        const file = files.next();
        res.push({ filename: file.getName(), mimeType: file.getMimeType() });
      }
    }
    const text = res
      .map(
        ({ filename, mimeType }) =>
          `Filename is ${filename}. MimeType is ${mimeType}.`
      )
      .join("\n");
    return {
      jsonrpc: "2.0",
      result: { content: [{ type: "text", text }], isError: false },
    };
  } catch (err) {
    return {
      jsonrpc: "2.0",
      result: { content: [{ type: "text", text: err.message }], isError: true },
    };
  }
}

/**
 * This function retrieves events from the specific date on Google Calendar.
 *
 * This function is run by "tools/call".
 * "tools/call": The function name is required to be the same as the name declared at "tools/list".
 */
function search_schedule_on_Google_Calendar(args) {
  const { date } = args;
  try {
    const start = new Date(date);
    start.setHours(0, 0, 0, 0);
    const end = new Date(start);
    end.setDate(start.getDate() + 1);
    const events = CalendarApp.getDefaultCalendar().getEvents(start, end); // or CalendarApp.getCalendarById("###").getEvents(start, end);
    const timeZone = Session.getScriptTimeZone();
    const text = events
      .map(
        (e) =>
          `${Utilities.formatDate(
            e.getStartTime(),
            timeZone,
            "HH:mm"
          )}-${Utilities.formatDate(
            e.getEndTime(),
            timeZone,
            "HH:mm"
          )}: ${e.getTitle()}`
      )
      .join("\n");
    return {
      jsonrpc: "2.0",
      result: { content: [{ type: "text", text }], isError: false },
    };
  } catch (err) {
    return {
      jsonrpc: "2.0",
      result: { content: [{ type: "text", text: err.message }], isError: true },
    };
  }
}

/**
 * This function retrieves today's events on Google Calendar.
 *
 * This function is run by "resources/read".
 * "resources/read": The function name is required to be the same as the uri declared at "resources/list".
 */
function get_today_schedule() {
  const start = new Date();
  start.setHours(0, 0, 0, 0);
  const end = new Date(start);
  end.setDate(start.getDate() + 1);
  const events = CalendarApp.getDefaultCalendar().getEvents(start, end); // or CalendarApp.getCalendarById("###").getEvents(start, end);
  const timeZone = Session.getScriptTimeZone();
  const contents = events.map((e) => ({
    uri: e.getTitle(),
    mimeType: "text/plain",
    text: `${Utilities.formatDate(
      e.getStartTime(),
      timeZone,
      "HH:mm"
    )}-${Utilities.formatDate(
      e.getEndTime(),
      timeZone,
      "HH:mm"
    )}: ${e.getTitle()}`,
  }));
  return { jsonrpc: "2.0", result: { contents } };
}

这是一个简单的示例。这个脚本可以测试。但是,如果您想创建MCP服务器,请修改并创建以下值 serverResponse,以及针对您的情况的功能。

另一种方法

当然,您可以通过直接将此库复制并粘贴到脚本编辑器中来使用它。在这种情况下,请复制并粘贴此库的脚本。并修改如下。

return new MCPApp.mcpApp({ accessKey: "sample" }).server(object);

向

return new MCPApp({ accessKey: "sample" }).server(object);

4.网络应用

为了允许从MCP客户端访问,该项目使用使用Google Apps Script构建的Web Apps。 参考 MCP客户端可以使用HTTP POST请求访问MCP服务器。因此,Web Apps可以用作MCP服务器。

详细信息可以在 官方文件.

请按照以下步骤在脚本编辑器中部署Web App。

  1. 在脚本编辑器的右上角,单击“部署”->“新建部署”。
  2. 点击“选择类型”->“Web App”。
  3. 在“部署配置”下的字段中输入有关Web App的信息。
  4. 选择 “我” 为了 “执行方式”.
  5. 选择 “任何人” 为了 **“谁有权访问该应用程序:”**在这个示例中,一种简单的方法允许没有访问令牌的请求。但是,自定义API密钥用于访问Web应用程序。
  6. 单击“部署”。
  7. 在脚本编辑器的右上角,单击“部署”->“测试部署”。
  8. 复制Web应用程序URL。它类似于 https://script.google.com/macros/s/###/exec.

需要注意的是,当您修改Web应用程序的Google Apps脚本时,必须将部署修改为新版本。 这可确保修改后的脚本反映在Web应用程序中。请小心这件事。此外,您可以在我的报告中找到更多详细信息”为新IDE重新部署Web应用程序而不更改Web应用程序的URL".

5.准备测试:Claude Desktop

为了测试这个Web应用程序,使用了Claude Desktop。 参考 在这种情况下 claude_desktop_config.json 配置如下。请更换 https://script.google.com/macros/s/###/exec 使用您的Web应用程序URL。

在此示例中,值为 sample 用作访问Web App的访问密钥。现阶段似乎通过Claude Desktop的HTTP请求使用MCP服务器, mcp-remote 需要使用。

{
  "mcpServers": {
    "gas_web_apps": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://script.google.com/macros/s/###/exec?accessKey=sample"
      ],
      "env": {}
    }
  }
}

我还可以使用Copilot for Visual Studio Code用Web Apps测试MCP服务器。在这种情况下,将使用以下设置。

"mcp": {
  "inputs": [],
  "servers": {
    "gas_web_apps": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://script.google.com/macros/s/###/exec?accessKey=sample"
      ],
      "env": {}
    }
  }
}

6.测试

当Claude Desktop运行时,您可以看到 gas_web_apps MCP服务器的安装如下。

作用 search_files_on_Google_Drive 被称为 tools/call 方法来自MCP客户端。

作用 search_schedule_on_Google_Calendar 被称为 tools/call 方法来自MCP客户端。

作用 search_files_from_Google_Drive 被称为 prompts/get 方法来自MCP客户端。

作用 get_today_schedule 被称为 resources/read 方法来自MCP客户端。在此示例中,加载了上述3个事件。

选项

1.日志记录

此库中提供以下日志记录选项。

如果您想查看MCP客户端和MCP服务器之间的日志,请使用以下脚本。请准备一个示例电子表格,并将电子表格ID设置为 spreadsheetId。这样做将在电子表格中创建一个“日志”表,日志将存储在那里。这将有助于理解MCP客户端和MCP服务器之间的通信。

/**
 * This function is automatically run when the MCP client accesses Web Apps.
 */
function doPost(eventObject) {
  const spreadsheetId = "###"; // Please set the Spreadsheet ID.

  const object = {
    eventObject,
    serverResponse: getserverResponse_(),
    functions: getFunctions_(),
  };
  return new MCPApp.mcpApp({
    accessKey: "sample",
    log: true,
    spreadsheetId,
  }).server(object);
}

样品

图像传输:MCP服务器(Web应用程序/谷歌应用程序脚本)到MCP客户端(Gemini/Python)

此示例脚本将图像数据发送到MCP客户端。这篇文章的原文来自 https://medium.com/google-cloud/image-transfer-mcp-server-web-apps-google-apps-script-to-mcp-client-gemini-python-1fb22eb89dd2.

/**
 * This function retrieves an image by searching Google Drive.
 *
 * This function is run by "tools/call".
 * "tools/call": The function name is required to be the same as the name declared at "tools/list".
 */
function get_image(args) {
  const { filename } = args;
  let result;
  try {
    const files = DriveApp.searchFiles(
      `title contains '${filename}' and mimeType contains 'image' and trashed=false`
    );
    if (files.hasNext()) {
      const file = files.next();
      result = {
        content: [
          {
            type: "text",
            text: `Actual filename on Google Drive is ${file.getName()}.`,
          },
          {
            type: "image",
            data: Utilities.base64Encode(file.getBlob().getBytes()),
            mimeType: file.getMimeType(),
          },
        ],
        isError: false,
      };
    } else {
      result = {
        content: [{ type: "text", text: `There is no file of "${filename}".` }],
        isError: true,
      };
    }
  } catch (err) {
    result = { content: [{ type: "text", text: err.message }], isError: true };
  }
  return { jsonrpc: "2.0", result };
}

/**
 * Please set and modify the following JSON to your situation.
 * The key is the method from the MCP client.
 * The value is the object for returning to the MCP client.
 * ID is automatically set in the script.
 * The specification of this can be seen in the official document.
 * Ref: https://modelcontextprotocol.io/specification/2025-03-26
 */
function getserverResponse_() {
  return {
    /**
     * Response to "initialize"
     */
    initialize: {
      jsonrpc: "2.0",
      result: {
        protocolVersion: "2024-11-05", // or "2025-03-26"
        capabilities: {
          experimental: {},
          prompts: {
            listChanged: false,
          },
          resources: {
            subscribe: false,
            listChanged: false,
          },
          tools: {
            listChanged: false,
          },
        },
        serverInfo: {
          name: "sample server from MCPApp",
          version: "1.0.0",
        },
      },
    },

    /**
     * Response to "tools/list"
     */
    "tools/list": {
      jsonrpc: "2.0",
      result: {
        tools: [
          {
            name: "get_image", // <--- It is required to create a function of the same name as this.
            description: "Get image from Google Drive.",
            inputSchema: {
              type: "object",
              properties: {
                filename: {
                  description: "Get image of this filename from Google Drive.",
                  type: "string",
                },
              },
              required: ["filename"],
            },
          },
        ],
      },
    },
  };
}

/**
 * "tools/call": The function name is required to be the same as the name declared at "tools/list".
 * "resources/read": The function name is required to be the same as the uri declared at "resources/list".
 */
function getFunctions_() {
  return { "tools/call": { get_image } };
}

/**
 * This function is automatically run when the MCP client accesses Web Apps.
 */
function doPost(eventObject) {
  const object = {
    eventObject,
    serverResponse: getserverResponse_(),
    functions: getFunctions_(),
  };
  return new MCPApp.mcpApp({ accessKey: "sample" }).server(object);
}

备注

  • 关于 protocolVersion,当MCP客户端和MCP服务器之间不同时,会发生错误。例如,在当前阶段,Claude Desktop v0.9.3使用 protocolVersion 属于的 2024-11-05在这种情况下,当MCP服务器返回时 protocolVersion 属于的 2025-03-26 对于initialize方法,客户端不会返回任何响应。
  • 关于 result.content 对于从函数返回响应,响应值的类型取决于客户端。例如,在Claude Desktop的情况下,当类型 array 使用时,出现类似错误 Unsupported content type: array。这可能会在未来的更新中得到解决。
  • 当向MCP客户端发送大量数据时(Claude Deskop,类似以下错误 result exceeds maximum length of 1048576 发生。因此,在将数据发送到MCP客户端时请小心。但这可能会在未来的更新中得到解决。

许可证

麻省理工学院

作者

田奈克

捐赠

更新历史记录

  • v1.0.0(2025年5月8日)

    1. 首次发布。
  • v1.0.1(2025年5月9日)

    1. 日志中的一个错误已被删除。

顶部