创建您自己的 MCP Server 来通过 AI 控制 WordPress 托管

如果您管理大量 WordPress 网站,您总是在寻找下一个简单的方法来减少访问仪表板和点击一系列按钮所花费的时间。 MCP(Model Context Protocols)最近引起了广泛关注,我们决定探索 MCP 如何与 Kinsta API 结合,帮助管理如此多网站的代理机构。

创建您自己的 MCP Server 来通过 AI 控制 WordPress 托管

如果您管理大量 WordPress 网站,您总是在寻找下一个简单的方法来减少访问仪表板和点击一系列按钮所花费的时间。

MCP(Model Context Protocols)最近引起了广泛关注,我们决定探索 MCP 如何与 Kinsta API 结合,帮助管理如此多网站的代理机构。

在本文中,我们将带您构建一个实际的示例,展示如何将 Claude 等 AI 助手连接到 Kinsta API,以管理代理机构每天都在执行的 WordPress 托管 任务。

我们要构建的内容

我们要构建一个 MCP Server,它公开一组工具,以便 AI 助手可以执行以下操作:

  • 列出您账户下的所有 WordPress 网站
  • 显示特定网站的环境
  • 清除给定环境中的缓存
  • 克隆现有网站以创建新网站
  • 查看哪些插件和主题已过时或存在漏洞
  • 在特定环境上触发插件更新

服务器设置完成后,它会连接到 MCP 主机/客户端(本例中为 Claude for Desktop):

Claude calling MCP tools to retrieve external data during a prompt.

Claude 在提示过程中调用 MCP 工具来检索外部数据。

请注意它如何调用各种工具,然后返回以下响应:

Claude displaying a structured response generated from retrieved tool data.

Claude 显示从检索到的工具数据生成的结构化响应。

入门

在深入代码之前,先了解一下 MCP 如何融入这个设置会有所帮助。

MCP Server 位于 AI 助手和现有 API 之间。它不会替换 API 或改变其工作方式。相反,它公开一组 AI 可以在需要时调用的工具。每个工具对应一个特定操作,如列出网站或清除网站缓存。

当您在 AI 助手中提出问题时,它会决定这些工具中是否有相关的。如果有,助手会通过 MCP Server 调用该工具,Server 与 API 通信,然后将结果作为简单响应返回。没有任何内容会在未经您批准的情况下自动运行,并且只有您公开的工具可用。

在此示例中,MCP Server 与 Kinsta API 通信并公开一组有限的 WordPress 托管操作。不需要自定义 UI、后台自动化或特殊的 AI 设置。

前提条件

要跟随操作,您需要具备以下条件:

  • 稳定的 Node.js 环境
  • 基本的 TypeScript 熟悉度
  • 启用了 API 访问权限的 Kinsta 账户
  • 您的 Kinsta API 密钥和公司 ID

您不需要任何 MCP 经验,也不需要构建或训练 AI 模型。我们只关注将现有工具连接在一起。

设置项目

首先为项目创建一个新目录并初始化 Node.js 应用:

mkdir kinsta-mcp
cd kinsta-mcp
npm init -y

接下来,安装 MCP SDK 和我们使用的一小部分依赖项:

npm install @modelcontextprotocol/sdk zod@3
npm install -D typescript @types/node

创建基本项目结构:

mkdir src
touch src/index.ts

然后更新您的 package.json,以便 Node 可以运行构建后的服务器:

{
  "name": "kinsta-mcp-server",
  "version": "1.0.0",
  "description": "MCP server for managing WordPress sites via the Kinsta API",
  "type": "module",
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "zod": "^3.24.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.0.0"
  }
}

最后,在项目根目录添加一个 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

一切就绪后,您就可以开始构建 MCP Server 本身了。

构建 MCP Server

项目设置完成后,现在到了构建 MCP Server 本身的时候。

我们首先导入所需的包并创建服务器实例。然后添加一个用于与 API 通信的小型辅助函数。之后,我们注册直接映射到 WordPress 托管操作的工具。

导入包并创建服务器

打开 src/index.ts 并在文件顶部添加以下导入:

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

这三个导入分别起到以下作用:

  • McpServer 是核心服务器,负责注册工具并处理来自 AI 客户端的请求
  • StdioServerTransport 允许服务器通过标准输入/输出进行通信,这是大多数桌面 AI 客户端连接的方式
  • zod 用于定义和验证每个工具接受的输入

接下来,定义一些 API 和凭证的常量:

const KINSTA_API_BASE = "https://api.kinsta.com/v2";
const KINSTA_API_KEY = process.env.KINSTA_API_KEY;
const KINSTA_COMPANY_ID = process.env.KINSTA_COMPANY_ID;

现在创建 MCP Server 实例:

const server = new McpServer({
  name: "kinsta",
  version: "1.0.0",
});

名称是服务器在 MCP 客户端中显示的方式。版本是可选的,但一旦您开始迭代就会很有用。

添加 API 请求的辅助函数

我们构建的大多数工具都需要向 API 发出 HTTP 请求。为了避免在各处重复该逻辑,请创建一个单一的辅助函数。在服务器设置下方添加以下内容:

async function kinstaRequest(
  endpoint: string,
  options: RequestInit = {}
): Promise {
  const url = `${KINSTA_API_BASE}${endpoint}`;
  const headers = {
    Authorization: `Bearer ${KINSTA_API_KEY}`,
    "Content-Type": "application/json",
    ...options.headers,
  };

  const response = await fetch(url, { ...options, headers });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Kinsta API error (${response.status}): ${errorText}`);
  }

  return response.json() as Promise;
}

实现工具执行

工具是 MCP Server 公开的主要内容。每个工具都是 AI 助手可以在您批准后调用的函数,用于执行特定任务。

在此服务器中,每个工具遵循相同的结构:

  • 工具名称(如 list_sites
  • 简短描述(这有助于助手知道何时使用它)
  • 输入模式(以便工具仅在有效输入时运行)
  • 处理程序函数(我们在其中调用 API 并格式化输出)

我们故意将响应格式化为纯文本。AI 助手在工具返回清晰、可读的输出(而不是转储原始 JSON)时表现最佳。

工具 1:列出网站

此工具检索您公司账户下的所有 WordPress 网站。在处理多个网站时,它通常是您想要的第一个操作,因为大多数其他操作都从网站 ID 开始。

API 响应包含每个网站的基本信息,因此我们定义一个简单的结构来处理:

interface Site {
  id: string;
  name: string;
  display_name: string;
  status: string;
  site_labels: Array;
}

interface ListSitesResponse {
  company: {
    sites: Site[];
  };
}

有了这个,我们就可以注册该工具了:

server.registerTool(
  "list_sites",
  {
    description:
      "Get all WordPress sites for your company. Returns site IDs, names, and status.",
    inputSchema: {},
  },
  async () => {
    const data = await kinstaRequest(
      `/sites?company=${KINSTA_COMPANY_ID}`
    );

    const sites = data.company.sites;

    if (!sites || sites.length === 0) {
      return {
        content: [
          { type: "text", text: "No sites found for this company." }
        ],
      };
    }

    const siteList = sites
      .map((site) => {
        const labels =
          site.site_labels?.map((l) => l.name).join(", ") || "none";

        return `• ${site.display_name} (${site.name})
  ID: ${site.id}
  Status: ${site.status}
  Labels: ${labels}`;
      })
      .join("\n\n");

    return {
      content: [
        {
          type: "text",
          text: `Found ${sites.length} site(s):\n\n${siteList}`,
        },
      ],
    };
  }
);

此工具不需要任何输入,因此输入模式为空。在处理程序内部,我们调用 API,检查空结果,然后将响应格式化为可读的文本。

我们返回的是一个简短的摘要,在聊天界面中效果很好,而不是返回原始 JSON。这使得 AI 助手可以轻松回答“我有哪些网站?”或“显示我所有的 WordPress 网站”之类的问题,无需任何额外的解析。

工具 2:获取环境

一旦有了网站 ID,下一个常见的步骤是检查其环境。此工具返回给定网站的所有环境,包括正式环境、预发布环境和高级预发布环境。

interface Environment {
  id: string;
  name: string;
  display_name: string;
  is_premium: boolean;
  primaryDomain?: {
    id: string;
    name: string;
  };
  container_info?: {
    php_engine_version: string;
  };
}

interface GetEnvironmentsResponse {
  site: {
    environments: Environment[];
  };
}

某些字段是可选的,如主域名或 PHP 版本,因此它们被相应地标记。该工具只接受网站 ID:

server.registerTool(
  "get_environments",
  {
    description:
      "Get environments (live, staging) for a specific site. Requires the site ID.",
    inputSchema: {
      site_id: z.string().describe("The site ID to get environments for"),
    },
  },
  async ({ site_id }) => {
    const data = await kinstaRequest(
      `/sites/${site_id}/environments`
    );

    const envs = data.site.environments;

    if (!envs || envs.length === 0) {
      return {
        content: [
          { type: "text", text: "No environments found for this site." }
        ],
      };
    }

    const envList = envs
      .map((env) => {
        const domain = env.primaryDomain?.name || "No domain";
        const php = env.container_info?.php_engine_version || "Unknown";
        const type = env.is_premium
          ? "Premium Staging"
          : env.name === "live"
            ? "Live"
            : "Staging";

        return `• ${env.display_name} (${type})
  ID: ${env.id}
  Domain: ${domain}
  PHP: ${php}`;
      })
      .join("\n\n");

    return {
      content: [
        {
          type: "text",
          text: `Found ${envs.length} environment(s):\n\n${envList}`,
        },
      ],
    };
  }
);

这通常是清除缓存、克隆网站或更新插件等操作之前的下一步。

工具 3:清除网站缓存

清除缓存是一项常规任务,但它也是异步操作。当您触发它时,API 会立即返回操作 ID,而缓存清除则在后台继续进行。

这是类型定义和工具函数:

interface OperationResponse {
  operation_id: string;
  message: string;
  status: number;
}
server.registerTool(
  "clear_site_cache",
  {
    description:
      "Clear the cache for a site environment. Requires the environment ID.",
    inputSchema: {
      environment_id: z
        .string()
        .describe("The environment ID to clear cache for"),
    },
  },
  async ({ environment_id }) => {
    const data = await kinstaRequest(
      "/sites/tools/clear-cache",
      {
        method: "POST",
        body: JSON.stringify({ environment_id }),
      }
    );

    return {
      content: [
        {
          type: "text",
          text: `Cache clear initiated!

Operation ID: ${data.operation_id}
Message: ${data.message}

Use get_operation_status to check progress.`,
        },
      ],
    };
  }
);

该工具不等待操作完成,而是立即返回操作 ID。这保持了交互的快速性,并允许 AI 助手在需要时进行后续跟进。

工具 4:克隆网站

克隆网站是代理机构经常使用的操作之一,特别是从模板工作或为新客户创建网站时。您不需要从头开始,而是使用现有环境并基于它创建一个新网站。

响应使用我们之前看到的相同操作结构,因此无需再次定义。该工具需要新网站的显示名称和要克隆的环境 ID:

server.registerTool(
  "clone_site",
  {
    description:
      "Clone an existing site environment to create a new site. Great for spinning up new client sites from a template.",
    inputSchema: {
      display_name: z
        .string()
        .describe("Name for the new cloned site"),
      source_env_id: z
        .string()
        .describe("The environment ID to clone from"),
    },
  },
  async ({ display_name, source_env_id }) => {
    const data = await kinstaRequest(
      "/sites/clone",
      {
        method: "POST",
        body: JSON.stringify({
          company: KINSTA_COMPANY_ID,
          display_name,
          source_env_id,
        }),
      }
    );

    return {
      content: [
        {
          type: "text",
          text: `Site clone initiated!

New site: ${display_name}
Operation ID: ${data.operation_id}
Message: ${data.message}

Use get_operation_status to check progress.`,
        },
      ],
    };
  }
);

此工具与其他工具配合使用特别有用。例如,AI 助手可以克隆网站,然后在操作完成后立即列出环境或检查插件状态。

工具 5:获取操作状态

由于某些操作是异步运行的,我们需要一种方法来检查它们的进度。这就是该工具的用途。

interface OperationStatusResponse {
  status?: number;
  message?: string;
}

server.registerTool(
  "get_operation_status",
  {
    description:
      "Check the status of an async operation (cache clear, site clone, etc.)",
    inputSchema: {
      operation_id: z
        .string()
        .describe("The operation ID to check"),
    },
  },
  async ({ operation_id }) => {
    const response = await fetch(
      `${KINSTA_API_BASE}/operations/${encodeURIComponent(operation_id)}`,
      {
        headers: {
          Authorization: `Bearer ${KINSTA_API_KEY}`,
        },
      }
    );

    const data: OperationStatusResponse = await response.json();

    if (response.status === 200) {
      return {
        content: [
          {
            type: "text",
            text: `Operation completed successfully!

Message: ${data.message || "Operation finished"}`,
          },
        ],
      };
    }

    if (response.status === 202) {
      return {
        content: [
          {
            type: "text",
            text: `Operation still in progress...

Message: ${data.message || "Processing"}`,
          },
        ],
      };
    }

    return {
      content: [
        {
          type: "text",
          text: `Operation status: ${response.status}

Message: ${data.message || "Unknown status"}`, }, ], }; } );


### 工具 6:获取所有网站的插件

当您管理多个 WordPress 网站时,插件通常是问题开始出现的地方。此工具通过查看整个公司账户的插件(而不是一次一个网站)来解决这个问题。

API 返回大量信息,包括每个插件安装在哪些环境、是否有可用更新、以及是否有版本被标记为存在漏洞。为了处理这些数据,我们定义了以下结构:

interface PluginEnvironment { id: string; site_display_name: string; display_name: string; plugin_status: string; plugin_update: string | null; plugin_version: string; is_plugin_version_vulnerable: boolean; plugin_update_version: string | null; }

interface Plugin { name: string; title: string; latest_version: string | null; is_latest_version_vulnerable: boolean; environment_count: number; update_count: number; environments: PluginEnvironment[]; }

interface GetPluginsResponse { company: { plugins: { total: number; items: Plugin[]; }; }; }


该工具本身不接受任何输入:

server.registerTool( "get_plugins", { description: "Get all WordPress plugins across all sites. Shows which plugins have updates available or security vulnerabilities.", inputSchema: {}, }, async () => { const data = await kinstaRequest( /company/${KINSTA_COMPANY_ID}/wp-plugins );

const plugins = data.company.plugins.items;

if (!plugins || plugins.length === 0) {
  return {
    content: [
      { type: "text", text: "No plugins found." }
    ],
  };
}

const sorted = [...plugins].sort(
  (a, b) => b.update_count - a.update_count
);

const pluginList = sorted.slice(0, 20).map((plugin) => {
  const status =
    plugin.update_count > 0
      ? `⚠️ ${plugin.update_count} site(s) need update`
      : "✅ Up to date";

  const vulnerable =
    plugin.is_latest_version_vulnerable ? " 🔴 VULNERABLE" : "";

  return `• ${plugin.title} (${plugin.name})${vulnerable}

Latest: ${plugin.latest_version || "unknown"} Installed on: ${plugin.environment_count} environment(s) ${status}`; }).join("\n\n");

const outdatedCount = plugins.filter(
  (p) => p.update_count > 0
).length;

return {
  content: [
    {
      type: "text",
      text: `Found ${data.company.plugins.total} plugins (${outdatedCount} have updates available):\n\n${pluginList}`,
    },
  ],
};

} );


### 工具 7:获取所有网站的主题

主题有着与插件类似的问题,但检查频率往往更低。此工具的工作方式与插件工具相同,但专注于 WordPress 主题。

响应结构与插件端点类似,只是包含主题特定的字段:

interface ThemeEnvironment { id: string; site_display_name: string; display_name: string; theme_status: string; theme_update: string | null; theme_version: string; is_theme_version_vulnerable: boolean; theme_update_version: string | null; }

interface Theme { name: string; title: string; latest_version: string | null; is_latest_version_vulnerable: boolean; environment_count: number; update_count: number; environments: ThemeEnvironment[]; }

interface GetThemesResponse { company: { themes: { total: number; items: Theme[]; }; }; }


### 工具 8:更新插件

列出问题很有用,但最终您需要修复它们。此工具允许您更新特定环境上的特定插件。

更新端点返回与之前相同的异步操作结构,因此我们可以跳过它。以下是工具定义:

server.registerTool( "update_plugin", { description: "Update a specific plugin to a new version on a site environment.", inputSchema: { environment_id: z .string() .describe("The environment ID where the plugin is installed"), plugin_name: z .string() .describe("The plugin name/slug (e.g., 'akismet', 'elementor')"), update_version: z .string() .describe("The version to update to (e.g., '5.3')"), }, }, async ({ environment_id, plugin_name, update_version }) => { const data = await kinstaRequest( /sites/environments/${environment_id}/plugins, { method: "PUT", body: JSON.stringify({ name: plugin_name, update_version, }), } );

return {
  content: [
    {
      type: "text",
      text: `Plugin update initiated!

Plugin: ${plugin_name} Target version: ${update_version} Operation ID: ${data.operation_id} Message: ${data.message}

Use get_operation_status to check progress.`, }, ], }; } );


与缓存清除和网站克隆一样,更新是异步运行的。返回操作 ID 让 AI 助手可以跟踪进度,而不是假设更新立即完成。

## 运行服务器

注册完所有工具后,最后一步是启动 MCP 服务器并使其对 AI 客户端可用。

在文件底部,添加使用 [STDIO 传输](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) 连接服务器的主函数:

async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Kinsta MCP Server running on stdio"); }

main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });


这告诉 MCP 服务器通过标准输入和输出监听请求。它使服务器可以被 MCP 兼容的桌面客户端发现。

这里有一个重要的细节是日志记录。因为此服务器通过 STDIO 通信,所有日志必须写入 stderr。写入 stdout 可能会干扰 MCP 消息并破坏连接。

接下来,构建项目:

npm run build


这会将 TypeScript 文件编译到 build 目录中,并使入口点可执行。

构建完成后,服务器就可以由 MCP 客户端启动了。您可以访问 [GitHub 上的完整代码](https://github.com/olawanlejoel/mcp-server-demo-kinsta-api)。

## 使用 Claude for Desktop 测试您的服务器

要使用您的 MCP 服务器,Claude for Desktop 需要知道如何启动它。打开 Claude Desktop 配置文件:

~/Library/Application Support/Claude/claude_desktop_config.json


如果文件不存在则创建它。如果您使用的是 VS Code,可以直接从终端打开它:

code ~/Library/Application\ Support/Claude/claude_desktop_config.json


在文件内,在 mcpServers 键下添加您的 MCP 服务器。例如:

{ "mcpServers": { "kinsta": { "command": "node", "args": ["/ABSOLUTE/PATH/TO/mcp-server-demo-kinsta-api/build/index.js"], "env": { "KINSTA_API_KEY": "your-api-key-here", "KINSTA_COMPANY_ID": "your-company-id-here" } } } }


此配置告诉 Claude for Desktop 有一个名为 kinsta 的 MCP 服务器,它应该使用 Node.js 启动,入口点是构建的 index.js 文件。

确保路径指向 build 目录中的编译文件,而不是 TypeScript 源文件。保存文件并重启 Claude for Desktop。

### 验证连接

Claude 重启后,打开一个新聊天。点击输入字段旁边的 **+** 图标,然后悬停在 **连接器** 上。您应该会看到列出的 MCP 服务器。

![在 Claude 中注册 MCP 服务器以启用工具访问。](/uploads/872c2239-99b2-456b-82db-63f6d4edb5ad.webp)

*在 Claude 中注册 MCP 服务器。*

服务器连接后,您可以立即开始使用它。Claude 会决定使用哪个工具,传递所需的输入,并以纯文本形式返回结果。

![在 Claude 中使用 MCP 驱动的流程更新 WordPress 插件。](/uploads/452de8c1-4f24-4132-9b67-775f4201696e.webp)

*在 Claude 中使用 MCP 驱动的流程更新 WordPress 插件。*

## 与您已有的工具交互的不同方式

目前正在改变的不是底层工具。API 仍然是 API。托管平台的工作方式仍然相同。改变的是我们与它们交互的方式。

AI 工具开始感觉不再像聊天框,而更像是一种界面。这个 MCP 服务器是这种转变的一个小例子。它没有引入新功能,而是以符合人们实际工作方式的方式暴露现有功能。

接下来会怎样取决于您。您可以保持简单,只做读取操作。您也可以添加更多带有审批和防护措施的自动化流程。或者您可以将同一个服务器连接到工作流程中的其他工具。

在探索此类新工具和工作流程时,拥有可靠的托管基础非常重要。您最不想看到的是因为停机或性能问题而浪费时间,而不是建设和改进您的网站。

Kinsta 为 WordPress 提供托管托管服务,即使您不在线也能保持网站可靠运行。您可以探索我们的[托管计划](https://kinsta.com/pricing/)或[联系我们的销售团队](https://kinsta.com/talk-to-sales/)找到适合您的方案。
ESC 关闭