我在過去三個月內為客戶項目構建 MCP 伺服器,現在我確信這是每個 CMS 在兩年內都將被訪問的方式。不是通過儀表板。不是通過人類手動調用的 REST API。而是通過理解您的內容模型並代表您讀取、創建和更新內容的 AI 代理。

模型上下文協議(MCP)是 Anthropic 在 2024 年末發布的開放標準。它為 AI 模型提供了一種結構化的方式來與外部工具和數據源進行交互。可以把它看作是 LLM 和...任何東西之間的通用適配器。您的數據庫。您的文件系統。您的 CMS。

本文將介紹為三個受歡迎的 CMS 後端構建 MCP 伺服器的過程:Payload CMS、Supabase(用作無頭 CMS)和 WordPress。我們將涵蓋架構、實際代碼,以及我在此過程中遇到的坑。

目錄

什麼是 MCP 以及為什麼您應該關注

模型上下文協議本質上是一個基於 JSON-RPC 2.0 的協議,定義了 AI 模型如何與外部系統通信。在 MCP 出現之前,如果您想讓 Claude 或 GPT 與您的 CMS 交互,您需要為每個模型構建一個自定義的函數調用集成。不同的模式、不同的身份驗證流程、不同的一切。

MCP 標準化了這一點。您構建一個伺服器,任何 MCP 兼容的客戶端(Claude Desktop、Cursor、Cline、自定義代理)都可以連接到它。

以下是為什麼這對 CMS 工作特別有趣的原因:

  • 內容團隊可以要求 AI「找出上個月發布的所有提及 React 的博客文章」,並從他們實際的 CMS 獲得真實結果
  • 開發人員可以讓他們的代碼助手無需離開編輯器即可創建和更新內容條目
  • 編輯工作流程可以自動化——一個 AI 代理可以起草內容、根據您的風格指南檢查內容,並通過現有工作流程發布內容

該協議定義了三個原語:

  1. 工具 ——AI 可以調用的函數(如 create_postupdate_entrysearch_content
  2. 資源 ——AI 可以讀取的數據(如您的內容模式、最近的文章、媒體庫)
  3. 提示 ——常見操作的可重用提示模板

對於 CMS 集成,您主要會使用工具和資源。

CMS 集成的 MCP 架構

在我們開始編寫代碼之前,讓我們理解該架構。MCP 伺服器位於 AI 客戶端和您的 CMS 之間:

[AI Client] <--MCP Protocol--> [MCP Server] <--REST/GraphQL/SDK--> [Your CMS]
   (Claude)      (stdio/SSE)      (Node.js)       (HTTP/DB)        (Payload/WP/Supabase)

MCP 伺服器公開了映射到您的 CMS 操作的工具和資源。傳輸層可以是:

  • stdio ——用於本地開發,AI 客戶端將 MCP 伺服器作為子進程生成
  • SSE(Server-Sent Events) ——用於遠程伺服器,使用帶有 SSE 的 HTTP 進行流式傳輸
  • Streamable HTTP ——2025 年規範修訂版中的較新傳輸選項

對於 CMS 伺服器,我建議從 stdio 開始進行開發,然後轉移到 SSE 或 Streamable HTTP 進行生產部署,其中 MCP 伺服器在您的基礎設施中與 CMS 一起運行。

基本伺服器框架

每個 MCP 伺服器都遵循相同的模式。這是使用官方 @modelcontextprotocol/sdk 的 TypeScript 框架:

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

const server = new McpServer({
  name: "my-cms-mcp",
  version: "1.0.0",
});

// 定義一個工具
server.tool(
  "search_content",
  "Search for content entries by query",
  {
    query: z.string().describe("Search query"),
    collection: z.string().describe("Collection/post type to search"),
    limit: z.number().optional().default(10),
  },
  async ({ query, collection, limit }) => {
    // Your CMS-specific logic here
    const results = await searchCMS(query, collection, limit);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  }
);

// 定義一個資源
server.resource(
  "schema",
  "cms://schema",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(await getCMSSchema()),
      },
    ],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

這是基礎。現在讓我們構建真實的實現。

為 Payload CMS 構建 MCP 伺服器

Payload 是我最喜歡的無頭項目 CMS(我們在我們的 無頭 CMS 開發工作 中大量使用它)。它是 TypeScript 原生的,具有出色的 API,其基於集合的架構完美地映射到 MCP 工具。

設置 Payload MCP 伺服器

首先,安裝依賴項:

mkdir payload-mcp-server && cd payload-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

這是完整的實現。Payload 的 REST API 足夠簡潔,這出人意料地直接:

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

const PAYLOAD_URL = process.env.PAYLOAD_URL || "http://localhost:3000";
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || "";

const headers = {
  "Content-Type": "application/json",
  Authorization: `users API-Key ${PAYLOAD_API_KEY}`,
};

async function payloadFetch(path: string, options: RequestInit = {}) {
  const res = await fetch(`${PAYLOAD_URL}/api${path}`, {
    ...options,
    headers: { ...headers, ...options.headers },
  });
  if (!res.ok) {
    throw new Error(`Payload API error: ${res.status} ${await res.text()}`);
  }
  return res.json();
}

const server = new McpServer({
  name: "payload-cms-mcp",
  version: "1.0.0",
});

// 列出所有集合
server.tool(
  "list_collections",
  "List all available Payload CMS collections",
  {},
  async () => {
    // Payload doesn't have a meta endpoint for this by default,
    // so we hit the config or hardcode known collections
    const collections = ["posts", "pages", "media", "categories"];
    return {
      content: [{ type: "text", text: JSON.stringify(collections) }],
    };
  }
);

// 在集合中搜索/列出文檔
server.tool(
  "find_documents",
  "Find documents in a Payload collection with optional filtering",
  {
    collection: z.string().describe("Collection slug (e.g., 'posts', 'pages')"),
    where: z.string().optional().describe("Payload where query as JSON string"),
    limit: z.number().optional().default(10),
    page: z.number().optional().default(1),
    sort: z.string().optional().describe("Field to sort by, prefix with - for desc"),
  },
  async ({ collection, where, limit, page, sort }) => {
    const params = new URLSearchParams();
    params.set("limit", String(limit));
    params.set("page", String(page));
    if (sort) params.set("sort", sort);
    if (where) {
      try {
        const whereObj = JSON.parse(where);
        // Convert to Payload query string format
        for (const [key, val] of Object.entries(whereObj)) {
          if (typeof val === "object" && val !== null) {
            for (const [op, v] of Object.entries(val as Record<string, unknown>)) {
              params.set(`where[${key}][${op}]`, String(v));
            }
          } else {
            params.set(`where[${key}][equals]`, String(val));
          }
        }
      } catch {
        return { content: [{ type: "text", text: "Invalid where JSON" }] };
      }
    }
    const data = await payloadFetch(`/${collection}?${params}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 通過 ID 獲取單個文檔
server.tool(
  "get_document",
  "Get a single document by ID from a Payload collection",
  {
    collection: z.string(),
    id: z.string().describe("Document ID"),
  },
  async ({ collection, id }) => {
    const data = await payloadFetch(`/${collection}/${id}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 創建一個文檔
server.tool(
  "create_document",
  "Create a new document in a Payload collection",
  {
    collection: z.string(),
    data: z.string().describe("Document data as JSON string"),
  },
  async ({ collection, data }) => {
    const result = await payloadFetch(`/${collection}`, {
      method: "POST",
      body: data,
    });
    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
  }
);

// 更新一個文檔
server.tool(
  "update_document",
  "Update an existing document in a Payload collection",
  {
    collection: z.string(),
    id: z.string(),
    data: z.string().describe("Partial document data as JSON string"),
  },
  async ({ collection, id, data }) => {
    const result = await payloadFetch(`/${collection}/${id}`, {
      method: "PATCH",
      body: data,
    });
    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

連接到 Claude Desktop

將此添加到您的 Claude Desktop 配置(Mac 上的 ~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "payload-cms": {
      "command": "npx",
      "args": ["tsx", "/path/to/payload-mcp-server/src/index.ts"],
      "env": {
        "PAYLOAD_URL": "http://localhost:3000",
        "PAYLOAD_API_KEY": "your-api-key-here"
      }
    }
  }
}

重新啟動 Claude Desktop,現在您可以要求它做諸如「在我的 CMS 中查找所有草稿文章」或「創建一篇關於 MCP 伺服器的新博客文章」之類的事情。

為 Supabase 構建 MCP 伺服器

Supabase 很有趣,因為它本身不是一個 CMS——它是一個帶有 REST API 的 Postgres 數據庫。但許多團隊將其用作一個,特別是使用在 Next.js 中構建的自定義管理面板(我們在我們的 Next.js 開發實踐 中經常這樣做)。

Supabase MCP 伺服器實際上是三者中最強大的,因為您可以直接訪問 SQL。

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

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // Use service role for server-side
);

const server = new McpServer({
  name: "supabase-cms-mcp",
  version: "1.0.0",
});

// 列出所有表格(內容類型)
server.tool(
  "list_tables",
  "List all tables in the public schema",
  {},
  async () => {
    const { data, error } = await supabase.rpc("get_tables");
    // You'll need a Postgres function for this, or use:
    const { data: tables } = await supabase
      .from("information_schema.tables" as any)
      .select("table_name")
      .eq("table_schema", "public");
    return {
      content: [{ type: "text", text: JSON.stringify(tables, null, 2) }],
    };
  }
);

// 使用篩選器查詢內容
server.tool(
  "query_content",
  "Query rows from a table with optional filters",
  {
    table: z.string().describe("Table name"),
    select: z.string().optional().default("*").describe("Columns to select"),
    filters: z.array(z.object({
      column: z.string(),
      operator: z.enum(["eq", "neq", "gt", "lt", "gte", "lte", "like", "ilike", "is"]),
      value: z.string(),
    })).optional().describe("Array of filter conditions"),
    limit: z.number().optional().default(20),
    orderBy: z.string().optional(),
    ascending: z.boolean().optional().default(false),
  },
  async ({ table, select, filters, limit, orderBy, ascending }) => {
    let query = supabase.from(table).select(select).limit(limit);

    if (filters) {
      for (const f of filters) {
        query = query.filter(f.column, f.operator, f.value);
      }
    }

    if (orderBy) {
      query = query.order(orderBy, { ascending });
    }

    const { data, error } = await query;
    if (error) {
      return { content: [{ type: "text", text: `Error: ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 插入內容
server.tool(
  "insert_row",
  "Insert a new row into a table",
  {
    table: z.string(),
    data: z.string().describe("Row data as JSON string"),
  },
  async ({ table, data: rowData }) => {
    const parsed = JSON.parse(rowData);
    const { data, error } = await supabase.from(table).insert(parsed).select();
    if (error) {
      return { content: [{ type: "text", text: `Error: ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 全文搜索(Supabase 對於 CMS 使用的殺手級功能)
server.tool(
  "full_text_search",
  "Perform full-text search on a table column",
  {
    table: z.string(),
    column: z.string().describe("Column with tsvector index"),
    query: z.string().describe("Search query"),
    limit: z.number().optional().default(10),
  },
  async ({ table, column, query, limit }) => {
    const { data, error } = await supabase
      .from(table)
      .select()
      .textSearch(column, query)
      .limit(limit);
    if (error) {
      return { content: [{ type: "text", text: `Error: ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

我喜歡 Supabase 方法的一點:因為 Supabase 公開了您的 Postgres 模式,您可以創建一個為 AI 提供完整模式定義的資源。然後 AI 確切地知道存在哪些字段、它們是什麼類型的,以及表如何相互關聯。這使其查詢的準確性大大提高。

為 WordPress 構建 MCP 伺服器

WordPress。在 2025 年仍為 43% 的網絡提供支持。無論您喜歡還是不喜歡,您都會遇到它。

WordPress REST API 實際上對於 MCP 集成是可靠的。棘手的部分是身份驗證——WordPress 有大約五種不同的身份驗證方法,其中沒有一種非常好。

對於 MCP 伺服器,我建議使用應用程序密碼(自 5.6 版以來內置於 WordPress):

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

const WP_URL = process.env.WP_URL!;
const WP_USER = process.env.WP_USER!;
const WP_APP_PASSWORD = process.env.WP_APP_PASSWORD!;

const authHeader = `Basic ${Buffer.from(`${WP_USER}:${WP_APP_PASSWORD}`).toString("base64")}`;

async function wpFetch(endpoint: string, options: RequestInit = {}) {
  const res = await fetch(`${WP_URL}/wp-json/wp/v2${endpoint}`, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      Authorization: authHeader,
      ...options.headers,
    },
  });
  if (!res.ok) {
    throw new Error(`WP API error: ${res.status} ${await res.text()}`);
  }
  return res.json();
}

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

server.tool(
  "search_posts",
  "Search WordPress posts",
  {
    search: z.string().optional().describe("Search query"),
    status: z.enum(["publish", "draft", "pending", "private", "any"]).optional().default("any"),
    per_page: z.number().optional().default(10),
    categories: z.array(z.number()).optional(),
    tags: z.array(z.number()).optional(),
    after: z.string().optional().describe("ISO 8601 date -- only posts after this date"),
    before: z.string().optional().describe("ISO 8601 date -- only posts before this date"),
  },
  async (params) => {
    const searchParams = new URLSearchParams();
    if (params.search) searchParams.set("search", params.search);
    if (params.status) searchParams.set("status", params.status);
    searchParams.set("per_page", String(params.per_page));
    if (params.categories) searchParams.set("categories", params.categories.join(","));
    if (params.tags) searchParams.set("tags", params.tags.join(","));
    if (params.after) searchParams.set("after", params.after);
    if (params.before) searchParams.set("before", params.before);

    const posts = await wpFetch(`/posts?${searchParams}`);
    // Strip HTML from content for cleaner AI consumption
    const cleaned = posts.map((p: any) => ({
      id: p.id,
      title: p.title.rendered,
      slug: p.slug,
      status: p.status,
      date: p.date,
      excerpt: p.excerpt.rendered.replace(/<[^>]*>/g, ""),
      content: p.content.rendered.replace(/<[^>]*>/g, ""),
      categories: p.categories,
      tags: p.tags,
    }));
    return {
      content: [{ type: "text", text: JSON.stringify(cleaned, null, 2) }],
    };
  }
);

server.tool(
  "create_post",
  "Create a new WordPress post",
  {
    title: z.string(),
    content: z.string().describe("Post content in HTML"),
    status: z.enum(["publish", "draft", "pending"]).optional().default("draft"),
    categories: z.array(z.number()).optional(),
    tags: z.array(z.number()).optional(),
    excerpt: z.string().optional(),
  },
  async (params) => {
    const result = await wpFetch("/posts", {
      method: "POST",
      body: JSON.stringify(params),
    });
    return {
      content: [{
        type: "text",
        text: `Created post "${result.title.rendered}" (ID: ${result.id}, Status: ${result.status})`,
      }],
    };
  }
);

server.tool(
  "update_post",
  "Update an existing WordPress post",
  {
    id: z.number().describe("Post ID"),
    title: z.string().optional(),
    content: z.string().optional(),
    status: z.enum(["publish", "draft", "pending", "trash"]).optional(),
    excerpt: z.string().optional(),
  },
  async ({ id, ...updates }) => {
    const result = await wpFetch(`/posts/${id}`, {
      method: "POST",
      body: JSON.stringify(updates),
    });
    return {
      content: [{
        type: "text",
        text: `Updated post "${result.title.rendered}" (ID: ${result.id})`,
      }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

WordPress MCP 的最大坑:內容以呈現的 HTML 形式返回。您會想要剝離標籤以供 AI 的上下文窗口使用,否則您會在 <p> 標籤上燃燒令牌。我在看著 Claude 在閱讀五篇博客文章時燃燒 50k 個令牌後才艱難地學到了這一點。

比較三種方法

特性 Payload CMS Supabase WordPress
身份驗證複雜性 簡單(API 密鑰) 簡單(服務角色密鑰) 中等(應用程序密碼)
模式感知 良好(類型化集合) 出色(完整 Postgres 模式) 差(無模式端點)
內容格式 乾淨的 JSON 乾淨的 JSON HTML 繁重,需要清理
寫入支持 出色 出色 良好(某些塊的怪癖)
媒體處理 通過 API 上傳 存儲 API REST 媒體端點
查詢靈活性 良好(Payload 查詢) 出色(SQL 級篩選) 有限(WP_Query 參數)
自託管選項 是(或雲)
典型設置時間 1-2 小時 1-2 小時 2-3 小時
最適合 現代無頭項目 自定義內容結構 現有的 WP 網站

安全考慮

這是我幾乎忘記寫的部分,但論證上它是最重要的。

當您給 AI 代理對您的 CMS 的寫入訪問權限時,您信任它不會刪除所有內容。以下是如何避免受傷的方法:

最小權限原則

為具有最小權限的 MCP 伺服器創建一個專用 API 用戶/密鑰。在 Payload 中,創建一個只能訪問特定集合的角色。在 Supabase 中,使用行級別安全策略。在 WordPress 中,創建一個具有 Author 角色的用戶,而不是 Administrator。

為破壞性操作添加確認

我總是為創建/更新/刪除工具添加一個 dryRun 參數。設置為 true 時,該工具返回會發生什麼而不實際執行。然後 AI 可以在進行操作之前與用戶確認。

server.tool(
  "delete_document",
  "Delete a document (use dryRun first!)",
  {
    collection: z.string(),
    id: z.string(),
    dryRun: z.boolean().default(true).describe("If true, only preview the deletion"),
  },
  async ({ collection, id, dryRun }) => {
    const doc = await payloadFetch(`/${collection}/${id}`);
    if (dryRun) {
      return {
        content: [{
          type: "text",
          text: `DRY RUN: Would delete "${doc.title}" (${id}) from ${collection}`,
        }],
      };
    }
    await payloadFetch(`/${collection}/${id}`, { method: "DELETE" });
    return {
      content: [{ type: "text", text: `Deleted "${doc.title}" (${id})` }],
    };
  }
);

速率限制和審計日誌

記錄每個工具調用。認真的。當您的內容編輯在凌晨 2 點詢問為什麼有 47 篇關於企鵝的新草稿文章時,您會想知道您的 AI 代理做了什麼。

永遠不要直接公開數據庫憑證

MCP 伺服器是邊界。AI 永遠看不到您的數據庫密碼或 API 密鑰——它只看到您公開的工具。

實際用例

以下是我在生產中看到工作良好的模式:

內容遷移:一個 AI 代理從舊版 WordPress 網站讀取內容,將其轉換以匹配您的新 Payload 模式,並在新 CMS 中創建條目。我們為遷移 ~3,000 篇文章的客戶執行了此操作。手動工作需要數週才能完成,與人工審查一起只花了一天半。

SEO 審計:一個 AI 讀取所有發布的內容,檢查缺少的元描述、內容稀薄和斷裂的內部鏈接。將其發現作為結構化報告寫回 CMS。

批量內容更新:「在所有已發布頁面中將所有對『我們的舊品牌名稱』的引用更改為『我們的新品牌名稱』。」代理讀取、查找、提出更改建議並在獲得人工批准後更新。

內容日曆管理:一個知道您的發布時間表、檢查草稿中有什麼、並通過讀取 CMS 數據來提醒團隊關於即將到來的截止日期的代理。

如果您正在構建這樣的東西並需要幫助,我們在我們的 無頭 CMS 實踐 中擁有所有三個平台的深厚經驗。您也可以查看我們的 定價頁面 以了解這樣的項目的成本。

常見問題

什麼是模型上下文協議(MCP)? MCP 是 Anthropic 創建的開放協議,用於標準化 AI 模型如何與外部數據源和工具交互。它使用 JSON-RPC 2.0 並定義了一個客戶端-伺服器架構,其中 AI 應用程序(客戶端)連接到公開工具、資源和提示的伺服器。可以把它看作 AI 的 USB-C 端口——一個連接到許多不同系統的標準界面。

我可以將 MCP 與 ChatGPT 或僅與 Claude 一起使用嗎? MCP 由 Anthropic 創建,因此 Claude 通過 Claude Desktop 和 API 具有最佳的原生支持。但是該協議是開放的,MCP 客戶端適用於其他平台。OpenAI 在 2025 年 3 月宣布了 MCP 支持。Cursor、Cline、Windsurf 和幾個其他開發者工具也支持 MCP。您的伺服器適用於任何兼容的客戶端。

給予 AI 代理對我的 CMS 的寫入訪問權限是否安全? 在適當的防護措施下可以安全。使用具有最小權限的專用 API 憑證、為破壞性操作實現乾運行模式、添加審計日誌,並考慮要求人工批准發布操作。永遠不要給 MCP 伺服器管理員級別的訪問權限。像對待任何第三方集成一樣對待它——具有適當的信任邊界。

我需要將 MCP 伺服器與 CMS 分開託管嗎? 對於 stdio 傳輸(本地開發),MCP 伺服器在本地計算機上作為子進程運行。對於生產環境中具有遠程訪問的情況,您需要在某個地方託管它——這可以在與 CMS 相同的伺服器上、在 Docker 容器中與其並列,或作為單獨的服務。MCP 伺服器和您的 CMS 之間的延遲很重要,因此將它們共位放在一起是理想的。

MCP 與直接使用帶有函數調用的 CMS REST API 相比如何? MCP 添加了一個標準化的界面層。對於原始函數調用,您為每個 AI 提供商定義自定義模式,為每個提供商以不同方式處理身份驗證,並在提供商更改其 API 時重建。MCP 為您提供一個適用於任何兼容客戶端的伺服器。它還以標準化方式處理傳輸、錯誤格式化和資源發現。對於單個集成,開銷可能不值得。對於您將重複使用的任何東西,它絕對值得。

哪個 CMS 最適合 MCP? Supabase 為您提供最大的靈活性,因為您可以直接訪問數據庫級別並獲得完整的模式自省。Payload CMS 很出色,因為它的 TypeScript 優先方法和乾淨的 REST API 自然映射到 MCP 工具。WordPress 工作良好,但需要更多的 HTML 內容清理和更靈活的查詢 API。「最好」的選擇取決於您現有的技術棧。

我可以使用 MCP 在 CMS 中管理媒體上傳嗎? 是的,雖然這涉及的內容比文本內容多一些。對於 Payload 和 WordPress,您可以創建接受 base64 編碼文件數據或 URL 的工具,並使用各自的上傳 API。對於 Supabase,您將使用存儲 API。我建議創建單獨的 upload_mediaattach_media 工具,而不是嘗試在內容創建工具中處理文件上傳。

運行 MCP 伺服器要花多少錢? MCP 伺服器本身很輕量級——一個使用最少資源的簡單 Node.js 進程。真正的成本來自 AI 令牌在您的代理讀取和寫入內容時消耗。典型的內容操作可能使用 2,000-10,000 個令牌,取決於被處理的內容量。根據當前 2025 年定價(Claude Sonnet 每百萬輸入/輸出令牌 $3/$15),那每次操作幾分之一美分。託管 MCP 伺服器的基礎設施成本可忽略不計——一個小型 VPS 或容器實例綽綽有餘。