> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 04-9 跨工具整合與 MCP

> MCP 是把外部工具與資料源接進 agent 的標準協定。本單元涵蓋 MCP 解決的整合問題、server 設定驗證、信任等級判斷、與 CLI 內建工具的取捨，以及安全邊界。

export const ToolCompare = ({lang = "zh", tools = [], dimensions = [], defaultSelected, notes = []}) => {
  const UI = lang === "en" ? {
    allTools: "All",
    recommend: "Recommended",
    noSupport: "N/A",
    minOneToolW: "Select at least one tool",
    mobileLabel: "Tool",
    dimensionLbl: "Dimension",
    notes: "Notes"
  } : {
    allTools: "全選",
    recommend: "推薦",
    noSupport: "無對應",
    minOneToolW: "至少選一個工具",
    mobileLabel: "工具",
    dimensionLbl: "維度",
    notes: "注意事項"
  };
  const ACCENT_L = "#bf7551";
  const ACCENT_D = "#cf8a68";
  const REC_BDR_L = "rgba(191,117,81,0.45)";
  const REC_BG_L = "rgba(191,117,81,0.06)";
  const REC_BDR_D = "rgba(207,138,104,0.45)";
  const REC_BG_D = "rgba(207,138,104,0.08)";
  const looksLikePath = val => typeof val === "string" && (/[/\\.*:]/).test(val);
  const safeTools = Array.isArray(tools) ? tools : [];
  const safeDimensions = Array.isArray(dimensions) ? dimensions : [];
  if (safeTools.length === 0 || safeDimensions.length === 0) return null;
  const allIds = safeTools.map(t => t.id);
  const initSelected = Array.isArray(defaultSelected) && defaultSelected.length > 0 ? defaultSelected.filter(id => allIds.includes(id)) : allIds;
  const [selectedIds, setSelectedIds] = useState(initSelected.length > 0 ? initSelected : allIds);
  const [expandedCell, setExpandedCell] = useState(null);
  const [mobileTool, setMobileTool] = useState(initSelected[0] || allIds[0]);
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const detect = () => setIsDark(document.documentElement.classList.contains("dark"));
    detect();
    const obs = new MutationObserver(detect);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => obs.disconnect();
  }, []);
  const toggleTool = id => {
    setSelectedIds(prev => {
      const next = prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id];
      if (next.length === 0) return prev;
      if (expandedCell) {
        const [, cellToolId] = expandedCell.split(":");
        if (!next.includes(cellToolId)) setExpandedCell(null);
      }
      return next;
    });
  };
  const isAllSelected = selectedIds.length === allIds.length;
  const toggleAll = () => {
    if (isAllSelected) {
      setSelectedIds([allIds[0]]);
    } else {
      setSelectedIds(allIds);
    }
    setExpandedCell(null);
  };
  const visibleTools = safeTools.filter(t => selectedIds.includes(t.id));
  const cellKey = (dimId, toolId) => dimId + ":" + toolId;
  const toggleExpand = (dimId, toolId) => {
    const key = cellKey(dimId, toolId);
    setExpandedCell(prev => prev === key ? null : key);
  };
  const renderValue = cell => {
    if (cell.na) {
      return <span className="tc-na">{UI.noSupport}</span>;
    }
    const useMono = cell.mono === true || cell.mono !== false && looksLikePath(cell.value);
    return useMono ? <code className="tc-code">{cell.value}</code> : <span>{cell.value}</span>;
  };
  const css = `
  /* ── 根容器 ── */
  .tc-root {
    --tc-bg:           #FAF8F3;
    --tc-surface:      rgba(0,0,0,0.022);
    --tc-stripe:       rgba(0,0,0,0.028);
    --tc-border:       rgba(0,0,0,0.09);
    --tc-text:         #2b2722;
    --tc-dim:          #6f6a62;
    --tc-faint:        #8a8378;
    --tc-accent:       ${ACCENT_L};
    --tc-accent-bg:    ${REC_BG_L};
    --tc-accent-bdr:   ${REC_BDR_L};
    --tc-expand-bg:    rgba(0,0,0,0.016);
    --tc-code-bg:      rgba(0,0,0,0.055);
    --tc-na-color:     #9a9490;
    --tc-pill-on-bg:   rgba(191,117,81,0.10);
    --tc-pill-on-bdr:  rgba(191,117,81,0.35);
    --tc-pill-on-txt:  #a05c38;
    border: 1px solid var(--tc-border);
    border-radius: 14px;
    background: var(--tc-bg);
    color: var(--tc-text);
    overflow: hidden;
    font-size: 14px;
  }
  .dark .tc-root {
    --tc-bg:           #1b1a18;
    --tc-surface:      rgba(255,255,255,0.03);
    --tc-stripe:       rgba(255,255,255,0.025);
    --tc-border:       rgba(255,255,255,0.08);
    --tc-text:         #e7e3da;
    --tc-dim:          #a8a299;
    --tc-faint:        #706b64;
    --tc-accent:       ${ACCENT_D};
    --tc-accent-bg:    ${REC_BG_D};
    --tc-accent-bdr:   ${REC_BDR_D};
    --tc-expand-bg:    rgba(255,255,255,0.022);
    --tc-code-bg:      rgba(255,255,255,0.08);
    --tc-na-color:     #6e6b65;
    --tc-pill-on-bg:   rgba(207,138,104,0.12);
    --tc-pill-on-bdr:  rgba(207,138,104,0.35);
    --tc-pill-on-txt:  ${ACCENT_D};
  }

  /* ── 精簡篩選列 ── */
  .tc-filter-bar {
    display: flex;
    align-items: center;
    gap: 0;
    padding: 0 14px;
    border-bottom: 1px solid var(--tc-border);
    background: var(--tc-surface);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    /* 單行、不折行，高度由內容決定（約 36px） */
    flex-wrap: nowrap;
    white-space: nowrap;
    min-height: 36px;
  }
  .tc-filter-bar::-webkit-scrollbar { display: none; }

  /* 分隔竿（All 後面） */
  .tc-filter-sep {
    flex-shrink: 0;
    width: 1px;
    height: 14px;
    background: var(--tc-border);
    margin: 0 10px 0 6px;
    align-self: center;
  }

  /* pill 基底 — 極輕量文字標籤 */
  .tc-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 5px 9px;
    border-radius: 5px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--tc-faint);
    font: inherit;
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.01em;
    cursor: pointer;
    transition: color 0.12s, background 0.12s, border-color 0.12s;
    flex-shrink: 0;
    white-space: nowrap;
    /* 行高對齊 filter-bar */
    margin: 5px 1px;
  }
  .tc-pill:hover {
    color: var(--tc-text);
    background: rgba(0,0,0,0.04);
  }
  .dark .tc-pill:hover {
    background: rgba(255,255,255,0.05);
  }
  /* 選中態：細框 + 淡底 + 文字加深（非按鈕感，保持輕量） */
  .tc-pill-on {
    color: var(--tc-pill-on-txt);
    background: var(--tc-pill-on-bg);
    border-color: var(--tc-pill-on-bdr);
    font-weight: 600;
  }
  /* 全選 pill — 略小一點字型 */
  .tc-pill-all {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    padding: 4px 8px;
    color: var(--tc-dim);
  }
  .tc-pill-all.tc-pill-on {
    color: var(--tc-pill-on-txt);
  }
  /* 選中小圓點 */
  .tc-pill-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--tc-accent);
    flex-shrink: 0;
  }

  /* ── 桌面表格 ── */
  .tc-table-wrap {
    overflow: hidden;
  }
  /* Mintlify 的 MDX 渲染器會自動把 <table> 包進 data-table-wrapper：
   * 加 -mx-[var(--page-padding)] 負邊距 + w-[calc(100%+padding*2)] 全寬 + py-[1em]，
   * 讓表格往外溢出，撐破 tc-root 的圓角容器（2026-06-12 線上實證跑版）。
   * 中和它：邊距歸零、寬度收回 100%、把橫向捲動容器設在這層（sticky 左欄靠它）。 */
  .tc-root [data-table-wrapper] {
    margin: 0 !important;
    width: 100% !important;
    max-width: 100% !important;
    padding: 0 !important;
    overflow-x: auto;
    contain: none !important;
  }
  .tc-root [data-table-wrapper] > div {
    padding: 0 !important;
    margin: 0 !important;
  }
  .tc-root [data-table-wrapper]::-webkit-scrollbar { height: 4px; }
  .tc-root [data-table-wrapper]::-webkit-scrollbar-thumb { background: var(--tc-border); border-radius: 2px; }
  .tc-table {
    width: 100%;
    border-collapse: collapse;
    margin: 0 !important;
  }

  /* 表頭 */
  .tc-thead th {
    padding: 9px 15px;
    text-align: left;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--tc-faint);
    border-bottom: 2px solid var(--tc-border);
    background: var(--tc-surface);
    white-space: nowrap;
  }
  /* 維度欄（sticky 左欄） */
  .tc-thead th:first-child,
  .tc-td-dim {
    position: sticky;
    left: 0;
    z-index: 1;
  }
  .tc-thead th:first-child {
    width: 160px;
    min-width: 130px;
    background: var(--tc-surface);
    border-right: 1px solid var(--tc-border);
    z-index: 2;
  }

  /* 維度標籤欄 */
  .tc-td-dim {
    padding: 13px 15px;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--tc-dim);
    vertical-align: middle;
    white-space: nowrap;
    border-bottom: 1px solid var(--tc-border);
    border-right: 1px solid var(--tc-border);
    background: var(--tc-surface);
  }

  /* 斑馬紋：奇數維度列 */
  .tc-row-even .tc-td-dim,
  .tc-row-even .tc-td {
    background-color: var(--tc-stripe);
  }
  .tc-row-even .tc-td-dim {
    background: color-mix(in srgb, var(--tc-surface) 70%, var(--tc-stripe) 30%);
  }

  /* 資料儲存格 */
  .tc-td {
    padding: 0;
    border-bottom: 1px solid var(--tc-border);
    border-left: 1px solid var(--tc-border);
    vertical-align: top;
    min-width: 150px;
  }
  .tc-td-inner {
    display: flex;
    flex-direction: column;
  }

  /* 主值行（可點擊） */
  .tc-cell-btn {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    padding: 13px 15px;
    color: inherit;
    font: inherit;
    font-size: 13px;
    line-height: 1.5;
    cursor: pointer;
    transition: background 0.11s;
    -webkit-tap-highlight-color: transparent;
  }
  .tc-cell-btn:hover {
    background: rgba(0,0,0,0.03);
  }
  .dark .tc-cell-btn:hover {
    background: rgba(255,255,255,0.03);
  }
  .tc-cell-btn-on {
    background: var(--tc-expand-bg);
  }

  /* 推薦標記：左 3px accent 邊框 + 淡底 */
  .tc-td-recommend {
    border-left: 3px solid var(--tc-accent-bdr);
    background: var(--tc-accent-bg);
  }
  .tc-td-recommend .tc-cell-btn:hover {
    background: rgba(191,117,81,0.05);
  }

  /* recommend 角標：超小 badge */
  .tc-rec-badge {
    flex-shrink: 0;
    margin-top: 1px;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--tc-accent);
    border: 1px solid var(--tc-accent-bdr);
    border-radius: 3px;
    padding: 1px 4px;
    white-space: nowrap;
    opacity: 0.85;
  }

  /* na 樣式 */
  .tc-na {
    font-style: italic;
    color: var(--tc-na-color);
    font-size: 12.5px;
    opacity: 0.7;
  }

  /* 等寬值 */
  .tc-code {
    font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
    font-size: 12px;
    background: var(--tc-code-bg);
    border-radius: 3px;
    padding: 1px 4px;
    word-break: break-all;
  }

  /* 展開詳情列 */
  .tc-detail-row td {
    padding: 0;
    border-bottom: 1px solid var(--tc-border);
  }
  .tc-detail-cell {
    padding: 10px 15px 12px;
    font-size: 12.5px;
    line-height: 1.65;
    color: var(--tc-dim);
    background: var(--tc-expand-bg);
    border-top: 1px dashed var(--tc-border);
    border-left: 1px solid var(--tc-border);
  }
  .tc-detail-cell:first-child {
    border-left: none;
    font-weight: 700;
    color: var(--tc-faint);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    vertical-align: top;
    padding-top: 12px;
    background: var(--tc-surface);
    white-space: nowrap;
    width: 160px;
  }
  .tc-detail-recommend {
    border-left: 3px solid var(--tc-accent-bdr) !important;
    background: var(--tc-accent-bg) !important;
  }

  /* 展開箭頭 */
  .tc-chevron {
    flex-shrink: 0;
    margin-top: 3px;
    opacity: 0.35;
    transition: transform 0.14s, opacity 0.14s;
  }
  .tc-chevron-open {
    transform: rotate(90deg);
    opacity: 0.65;
  }

  /* ── 注腳 ── */
  .tc-notes {
    border-top: 1px solid var(--tc-border);
    padding: 10px 16px 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .tc-notes-label {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--tc-faint);
    margin-bottom: 3px;
  }
  .tc-note-item {
    font-size: 12.5px;
    color: var(--tc-dim);
    line-height: 1.55;
    padding-left: 14px;
    position: relative;
  }
  .tc-note-item::before {
    content: "*";
    position: absolute;
    left: 2px;
    color: var(--tc-faint);
  }

  /* ── 手機模式 ─────────────────────────────────────────── */
  .tc-mobile { display: none; }
  @media (max-width: 700px) {
    .tc-filter-bar { display: none; }
    .tc-table-wrap  { display: none; }
    .tc-mobile      { display: block; }

    .tc-mob-tabs {
      display: flex;
      overflow-x: auto;
      border-bottom: 1px solid var(--tc-border);
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
    }
    .tc-mob-tabs::-webkit-scrollbar { display: none; }
    .tc-mob-tab {
      flex: 1 0 auto;
      padding: 10px 16px;
      background: transparent;
      border: none;
      border-bottom: 2px solid transparent;
      color: var(--tc-dim);
      font: inherit;
      font-size: 13px;
      font-weight: 500;
      cursor: pointer;
      white-space: nowrap;
      transition: color 0.12s, border-color 0.12s;
    }
    .tc-mob-tab-on {
      color: var(--tc-accent);
      border-bottom-color: var(--tc-accent);
      font-weight: 700;
    }
    .tc-mob-cards {
      padding: 12px 0 4px;
    }
    .tc-mob-card {
      border-bottom: 1px solid var(--tc-border);
      padding: 12px 16px;
    }
    .tc-mob-card:last-child { border-bottom: none; }
    .tc-mob-dim {
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--tc-faint);
      margin-bottom: 6px;
    }
    .tc-mob-val {
      font-size: 13.5px;
      line-height: 1.55;
    }
    .tc-mob-rec {
      display: inline-block;
      margin-top: 6px;
      font-size: 9.5px;
      font-weight: 700;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--tc-accent);
      border: 1px solid var(--tc-accent-bdr);
      border-radius: 3px;
      padding: 1px 5px;
    }
    .tc-mob-detail {
      margin-top: 7px;
      font-size: 12.5px;
      line-height: 1.65;
      color: var(--tc-dim);
    }
    .tc-notes { padding: 11px 16px 13px; }
  }
  `;
  const renderDesktopTable = () => <div className="tc-table-wrap">
      <table className="tc-table">
        <thead className="tc-thead">
          <tr>
            <th>{UI.dimensionLbl}</th>
            {visibleTools.map(tool => <th key={tool.id}>{tool.label}</th>)}
          </tr>
        </thead>
        <tbody>
          {}
          {safeDimensions.flatMap((dim, dimIdx) => {
    const isAnyExpanded = visibleTools.some(t => expandedCell === cellKey(dim.id, t.id));
    const stripeClass = dimIdx % 2 === 1 ? " tc-row-even" : "";
    const rows = [];
    rows.push(<tr key={dim.id} className={"tc-row" + stripeClass}>
                <td className="tc-td-dim">{dim.label}</td>
                {visibleTools.map(tool => {
      const cell = (dim.cells || ({}))[tool.id] || ({});
      const key = cellKey(dim.id, tool.id);
      const isOpen = expandedCell === key;
      const hasDetail = !!cell.detail;
      const isRec = !!cell.recommend;
      return <td key={tool.id} className={"tc-td" + (isRec ? " tc-td-recommend" : "")}>
                      <div className="tc-td-inner">
                        <button type="button" className={"tc-cell-btn" + (isOpen ? " tc-cell-btn-on" : "")} onClick={hasDetail ? () => toggleExpand(dim.id, tool.id) : undefined} style={hasDetail ? {} : {
        cursor: "default"
      }} aria-expanded={hasDetail ? String(isOpen) : undefined}>
                          <span style={{
        flex: "1 1 0",
        minWidth: 0
      }}>
                            {renderValue(cell)}
                          </span>
                          {isRec && <span className="tc-rec-badge">{UI.recommend}</span>}
                          {hasDetail && <svg className={"tc-chevron" + (isOpen ? " tc-chevron-open" : "")} width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                              <polyline points="9 18 15 12 9 6" />
                            </svg>}
                        </button>
                      </div>
                    </td>;
    })}
              </tr>);
    if (isAnyExpanded) {
      const openToolId = visibleTools.find(t => expandedCell === cellKey(dim.id, t.id))?.id;
      const openCell = openToolId ? (dim.cells || ({}))[openToolId] || ({}) : {};
      if (openCell.detail) {
        const openTool = safeTools.find(t => t.id === openToolId);
        const isRec = !!openCell.recommend;
        rows.push(<tr key={dim.id + "-detail"} className="tc-detail-row">
                    <td className="tc-detail-cell">
                      {openTool ? openTool.label : ""}
                    </td>
                    <td colSpan={visibleTools.length} className={"tc-detail-cell" + (isRec ? " tc-detail-recommend" : "")}>
                      {openCell.detail}
                    </td>
                  </tr>);
      }
    }
    return rows;
  })}
        </tbody>
      </table>
    </div>;
  const renderMobile = () => {
    const activeTool = safeTools.find(t => t.id === mobileTool) || safeTools[0];
    return <div className="tc-mobile">
        <div className="tc-mob-tabs" role="tablist">
          {safeTools.map(tool => <button key={tool.id} type="button" role="tab" aria-selected={tool.id === mobileTool ? "true" : "false"} className={"tc-mob-tab" + (tool.id === mobileTool ? " tc-mob-tab-on" : "")} onClick={() => setMobileTool(tool.id)}>
              {tool.label}
            </button>)}
        </div>

        <div className="tc-mob-cards">
          {safeDimensions.map(dim => {
      const cell = (dim.cells || ({}))[activeTool.id] || ({});
      const isRec = !!cell.recommend;
      return <div key={dim.id} className="tc-mob-card">
                <div className="tc-mob-dim">{dim.label}</div>
                <div className="tc-mob-val">{renderValue(cell)}</div>
                {isRec && <div className="tc-mob-rec">{UI.recommend}</div>}
                {cell.detail && <div className="tc-mob-detail">{cell.detail}</div>}
              </div>;
    })}
        </div>
      </div>;
  };
  return <div className="tc-root">
      <style>{css}</style>

      {}
      <div className="tc-filter-bar">
        <button type="button" className={"tc-pill tc-pill-all" + (isAllSelected ? " tc-pill-on" : "")} onClick={toggleAll} aria-pressed={String(isAllSelected)}>
          {UI.allTools}
        </button>

        {}
        <span className="tc-filter-sep" aria-hidden="true" />

        {safeTools.map(tool => {
    const isOn = selectedIds.includes(tool.id);
    return <button key={tool.id} type="button" className={"tc-pill" + (isOn ? " tc-pill-on" : "")} onClick={() => toggleTool(tool.id)} aria-pressed={String(isOn)}>
              {isOn && <span className="tc-pill-dot" aria-hidden="true" />}
              {tool.label}
            </button>;
  })}
      </div>

      {}
      {visibleTools.length > 0 && renderDesktopTable()}

      {}
      {renderMobile()}

      {}
      {notes.length > 0 && <div className="tc-notes">
          <div className="tc-notes-label">{UI.notes}</div>
          {notes.map((note, i) => <div key={i} className="tc-note-item">{note}</div>)}
        </div>}
    </div>;
};

export const LearnerPrimer = ({items = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    title: "Before this unit, be honest with yourself",
    sub: "If you can't answer these, that gap is exactly what this unit closes."
  } : {
    title: "讀這個單元前，先誠實面對",
    sub: "這幾題答不出來，正是這個單元要替你補的洞。"
  };
  const css = `
  .lp-root{--lp-bg:#FAF7F1;--lp-surface:rgba(191,117,81,0.05);--lp-border:rgba(0,0,0,0.09);--lp-edge:rgba(191,117,81,0.42);--lp-text:#2b2722;--lp-dim:#6f6a62;--lp-accent:#bf7551;border:1px solid var(--lp-border);border-left:3px solid var(--lp-edge);border-radius:13px;background:var(--lp-bg);color:var(--lp-text);overflow:hidden;margin:1.25rem 0;}
  .dark .lp-root{--lp-bg:#1b1a18;--lp-surface:rgba(207,138,104,0.07);--lp-border:rgba(255,255,255,0.08);--lp-edge:rgba(207,138,104,0.5);--lp-text:#e7e3da;--lp-dim:#a8a299;--lp-accent:#cf8a68;}
  .lp-head{display:flex;align-items:center;gap:9px;padding:13px 18px 11px;border-bottom:1px solid var(--lp-border);background:var(--lp-surface);}
  .lp-ic{color:var(--lp-accent);flex-shrink:0;}
  .lp-htx{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .lp-title{font-size:14px;font-weight:650;line-height:1.3;letter-spacing:.01em;}
  .lp-sub{font-size:12px;color:var(--lp-dim);line-height:1.4;}
  .lp-list{list-style:none;margin:0;padding:10px 18px 14px;display:flex;flex-direction:column;gap:0;}
  .lp-item{display:flex;align-items:baseline;gap:11px;padding:7px 0;font-size:14px;line-height:1.6;border-top:1px solid var(--lp-border);}
  .lp-item:first-child{border-top:none;}
  .lp-mark{flex-shrink:0;color:var(--lp-accent);font-size:13px;font-weight:700;line-height:1.55;font-variant-numeric:tabular-nums;opacity:.85;}
  .lp-q{flex:1 1 0;min-width:0;color:var(--lp-text);}
  @media (max-width:620px){.lp-head{padding:12px 14px 10px;}.lp-list{padding:8px 14px 12px;}}
  `;
  return <div className="lp-root">
      <style>{css}</style>
      <div className="lp-head">
        <svg className="lp-ic" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></svg>
        <span className="lp-htx">
          <span className="lp-title">{t.title}</span>
          <span className="lp-sub">{t.sub}</span>
        </span>
      </div>
      <ul className="lp-list">
        {items.map((q, i) => <li className="lp-item" key={i}>
            <span className="lp-mark">{String(i + 1).padStart(2, "0")}</span>
            <span className="lp-q">{q}</span>
          </li>)}
      </ul>
    </div>;
};

<LearnerPrimer
  lang="zh"
  items={[
"MCP 是 fallback，不是預設",
"project scope 預設待核准",
"你圖快按下確認，惡意 server 就掛上了",
"OAuth scope 全開接受，server 拿到超過需要的權限",
"同一操作 CLI 與 MCP 混用，狀態分裂你還不知道",
"沒驗證的 MCP server，你讓外部字串直接進 context",
"裝了十個 server 但從不 review，你忘了為什麼裝",
"有 CLI 的場景先走 CLI，不要拿 MCP 當預設",
]}
/>

<Info>
  **這個單元解決什麼問題**

  MCP（Model Context Protocol）是把外部工具與資料源接進 agent 的標準協定。你會學到 MCP 解決什麼整合問題、怎麼設定並驗證一個 server、如何依信任等級與權限範圍選擇 server，以及它和 CLI、內建工具相比各自適用的場景。最後建立「什麼時候該走 MCP、什麼時候不該」的明確判準。
</Info>

## 學習目標

* [ ] 說明 MCP 是什麼、它解決哪種整合問題，以及為何需要一個跨工具協定。
* [ ] 設定並驗證一個 local MCP server（以 Claude Code 為主範本）。
* [ ] 依信任來源與權限範圍判斷是否引入某個 MCP server。
* [ ] 說明 MCP 與 CLI、內建工具的取捨，並知道何時優先走 CLI（連結 [04-10](/code-agent/customization/cli-first-vs-mcp)）。
* [ ] 辨識 MCP 的主要攻擊面並採取最小必要的防護措施（連結 [03-3](/code-agent/judgment/security-privacy-supply-chain)）。

***

## 1. MCP 是什麼

MCP（Model Context Protocol）是 Anthropic 在 2024 年提出的開放標準，定義「外部工具與資料源如何被 LLM 呼叫」。一個 MCP server 對外暴露 `tools`（可呼叫函式）、`resources`（可引用的資料）、`prompts`（可被執行的命令範本）三類介面；MCP client（如 Claude Code、Cursor、Claude Desktop、Codex 等）連上後把這些介面掛進自己的 context，模型就能像呼叫內建工具一樣呼叫它 \[1]。

它解決的具體問題：**整合碎片化**。如果每個 LLM 工具（Claude Code、Cursor、Codex、Antigravity...）都自己造一份「接 GitHub / 接 Slack / 接 Postgres」的介面，N×M 的開發與維護成本會把整合推向付費牆。MCP 把這個問題壓到 N+M：server 寫一次、跨多個 client 複用；client 內建 MCP 能力、不必為每個服務客製。

關鍵概念：

* **MCP server**：提供 `tools`、`resources`、`prompts` 的行程或服務。可以是本機 stdio 行程（`npx server-foo`），也可以是遠端 HTTP / WebSocket 服務。
* **MCP client**：模型端呼叫者。Claude Code 內建 MCP client，掛入 server 後把它提供的工具加入 `mcp__<server>__<tool>` 命名空間。
* **Transport**：client 與 server 間的傳輸協定。Claude Code 支援 stdio（本機行程）、http（含 `streamable-http` 別名，為最廣泛支援的雲端傳輸）、sse（deprecated）、ws（WebSocket，給需要主動推送事件的 server）\[1]。

<Tip>
  **MCP 與 Skill / Subagent 的分工**

  **MCP 是「接外部系統」的介面**。Skill 是「包裝程序」的容器，Subagent 是「隔離 context」的執行者。三者層級不同：MCP 給能力，Skill 給流程，Subagent 給環境。**同一個外部系統應該只挑一種接入方式**，不是 Skill 包 MCP 又另開 Subagent 跑它。
</Tip>

<Warning>
  **官方安全提示**

  Anthropic 在 MCP 章節明示「Verify you trust each server before connecting it. Servers that fetch external content can expose you to prompt injection risk」\[1]。這不是客套話。MCP server 從外部拉回的字串會直接進入模型 context，是 prompt injection 的高頻入口。
</Warning>

## 2. 接一個 server：設定、註冊、驗證

export const McpApprovalFlow = ({lang = "zh"}) => {
  const t = {
    zh: {
      title: "MCP server 連線與核准流程",
      nodes: [{
        id: "declare",
        label: "宣告 server",
        detail: "在 .mcp.json（project scope）或 ~/.claude.json（local / user scope）設定 server 的 type、url / command、env、headers。"
      }, {
        id: "scope",
        label: "Scope 判定",
        detail: "project scope 來自 .mcp.json，其 server 在 Claude Code 啟動時被標為「待核准」，需互動式 review 後才啟用。local / user scope 透過 claude mcp add CLI 直接寫入 ~/.claude.json，不走核准流程。"
      }, {
        id: "approve",
        label: "使用者核准",
        detail: "Claude Code 啟動時顯示 project scope 的 server 清單，使用者逐一確認。這是 CVE-2025-59536 後官方加的防護，阻止陌生 server 不知情自動掛上。"
      }, {
        id: "context",
        label: "工具進 context",
        detail: "核准後，server 提供的 tools / resources / prompts 以 mcp__<server>__<tool> 命名掛入 context。MCP tool search 預設開啟，工具不全量載入，而是 Claude 透過 ToolSearch 動態發現，降低 context 成本。"
      }, {
        id: "call",
        label: "模型呼叫工具",
        detail: "模型依任務需要呼叫 mcp__<server>__<tool>，Claude Code 把呼叫轉送給 server，server 回傳結果回填 context。回傳字串直接進 context，是 prompt injection 入口，需搭配第 5 節的安全邊界。"
      }],
      hint: "點擊各步驟查看細節"
    },
    en: {
      title: "MCP Server Connection and Approval Flow",
      nodes: [{
        id: "declare",
        label: "Declare server",
        detail: "Configure the server's type, url / command, env, and headers in .mcp.json (project scope) or ~/.claude.json (local / user scope)."
      }, {
        id: "scope",
        label: "Scope determination",
        detail: "Project-scope servers from .mcp.json are flagged as 'pending approval' when Claude Code starts and require interactive review before activating. Local / user scope servers added via 'claude mcp add' are written directly to ~/.claude.json with no approval step."
      }, {
        id: "approve",
        label: "User approval",
        detail: "Claude Code displays the project-scope server list on startup; the user reviews each one. This is the protection Anthropic added after CVE-2025-59536 to prevent unknown servers from silently attaching."
      }, {
        id: "context",
        label: "Tools enter context",
        detail: "After approval, the server's tools / resources / prompts are mounted in context under mcp__<server>__<tool> namespacing. MCP tool search is on by default: tools are not fully loaded upfront but are dynamically discovered by Claude via ToolSearch, reducing context cost."
      }, {
        id: "call",
        label: "Model calls tool",
        detail: "The model calls mcp__<server>__<tool> as the task requires. Claude Code forwards the call to the server; the server's response is fed back into context. Because response strings land directly in context, they are a prompt injection entry point -- require the security boundaries in Section 5."
      }],
      hint: "Click each step to see details"
    }
  };
  const {title, nodes, hint} = t[lang] || t.zh;
  const [active, setActive] = useState(null);
  const colors = {
    declare: "#6b8e7a",
    scope: "#bf7551",
    approve: "#a85f3f",
    context: "#6b7a8e",
    call: "#7a6b8e"
  };
  const step = nodes[active];
  const css = `
    .maf-root{font-family:inherit;margin:1.5rem 0}
    .maf-title{font-size:.85rem;font-weight:600;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-bottom:.9rem}
    .maf-flow{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap}
    .maf-node{padding:.45rem .85rem;border-radius:6px;font-size:.85rem;font-weight:600;color:#fff;cursor:pointer;border:2px solid transparent;transition:transform .15s,border-color .15s;white-space:nowrap}
    .maf-node:hover{transform:translateY(-2px)}
    .maf-node.active{border-color:#fff}
    .maf-arrow{color:#999;font-size:1.1rem;flex-shrink:0}
    .maf-detail{margin-top:.9rem;background:var(--maf-bg,#f5f0eb);border-left:3px solid var(--maf-accent,#bf7551);border-radius:0 6px 6px 0;padding:.75rem 1rem;font-size:.875rem;line-height:1.6;color:var(--maf-text,#3a3028)}
    .maf-detail-label{font-weight:700;margin-bottom:.35rem;color:var(--maf-accent,#bf7551)}
    .maf-hint{font-size:.78rem;color:#aaa;margin-top:.6rem}
    .dark .maf-root{--maf-bg:#2a2520;--maf-accent:#cf8a68;--maf-text:#d4ccc4}
  `;
  return <div className="maf-root">
      <style>{css}</style>
      <div className="maf-title">{title}</div>
      <div className="maf-flow">
        {nodes.map((n, i) => <>
            <div key={n.id} className={`maf-node${active === i ? " active" : ""}`} style={{
    background: colors[n.id]
  }} onClick={() => setActive(active === i ? null : i)}>
              {n.label}
            </div>
            {i < nodes.length - 1 && <span className="maf-arrow">→</span>}
          </>)}
      </div>
      {active !== null && <div className="maf-detail">
          <div className="maf-detail-label">{step.label}</div>
          {step.detail}
        </div>}
      <div className="maf-hint">{hint}</div>
    </div>;
};

<McpApprovalFlow lang="zh" />

Claude Code 提供三條安裝路徑（截至 2026-06，依官方 MCP 章節 \[1]）：

### 2.1 透過 `claude mcp add` CLI（最常用）

```bash theme={null}
# 1. 遠端 HTTP server
claude mcp add --transport http notion https://mcp.notion.com/mcp

# 2. 帶認證 header
claude mcp add --transport http secure-api https://api.example.com/mcp \
  --header "Authorization: Bearer your-token"

# 3. 本機 stdio server
claude mcp add --transport stdio --env AIRTABLE_API_KEY=YOUR_KEY airtable \
  -- npx -y airtable-mcp-server

# 4. 從 JSON 設定
claude mcp add-json weather-api '{"type":"http","url":"https://api.weather.com/mcp","headers":{"Authorization":"Bearer token"}}'
```

<Warning>
  **選項順序**

  所有選項（`--transport`、`--env`、`--scope`、`--header`）**必須在 server 名稱之前**；`--` 把名稱跟 server 的 command / args 分開 \[1]。混用會把 Claude 的 flag 跟 server 的 flag 撞在一起。
</Warning>

### 2.2 三種 scope

設定存在哪、是否團隊共享，由 `--scope` 決定 \[1]：

| Scope       | 載入範圍   | 共享     | 儲存位置                           |
| ----------- | ------ | ------ | ------------------------------ |
| `local`（預設） | 當前專案   | 否      | `~/.claude.json`（在該專案的 path 內） |
| `project`   | 當前專案   | 是（進版控） | 專案根目錄的 `.mcp.json`             |
| `user`      | 你的所有專案 | 否      | `~/.claude.json`               |

**重要的安全設計**：project scope 的 server（從 `.mcp.json` 來）在 Claude Code 啟動時會**先標為待核准**，需使用者互動式 review 後才啟用 \[1]。這是 CVE-2025-59536 之後官方加的防護：`.mcp.json` 不能在使用者不知情時自動核准陌生 server。

<Note>
  **「local scope」與「local settings」是兩件事**

  MCP local scope 存 `~/.claude.json`；一般的 local settings 是 `.claude/settings.local.json`。命名相近但層級不同 \[1]。
</Note>

### 2.3 手動編輯 `.mcp.json`

如果走 project scope 或想用環境變數展開：

```json theme={null}
{
  "mcpServers": {
    "api-server": {
      "type": "http",
      "url": "${API_BASE_URL:-https://api.example.com}/mcp",
      "headers": {
        "Authorization": "Bearer ${API_KEY}"
      },
      "timeout": 600000
    },
    "database-tools": {
      "command": "/path/to/server",
      "args": ["--config", "/path/to/config.json"],
      "env": {
        "DB_URL": "${DB_URL}"
      }
    }
  }
}
```

`${VAR}` 與 `${VAR:-default}` 兩種展開語法都支援；未設定且無預設會在 Claude Code 解析設定時直接 fail \[1]。`timeout` 是該 server 每次工具呼叫的硬性時鐘上限（毫秒），可用 `MCP_TIMEOUT` 環境變數覆寫。

### 2.4 驗證連線

裝完後跑：

```bash theme={null}
claude mcp list          # 列出所有 server 與狀態
claude mcp get github    # 看單一 server 的細節
```

在 Claude Code 內：

```
/mcp                      # 開啟 MCP 管理介面，看每個 server 的工具數、連線狀態
```

**驗證 SOP**：

<Steps>
  <Step title="確認 server 出現在 /mcp 列表">
    執行 `/mcp` 確認目標 server 狀態為已連線，工具數量與預期相符。
  </Step>

  <Step title="跑一次簡單呼叫">
    如 `mcp__notion__search` 帶簡單 query，確認模型能成功呼叫並取得回傳。
  </Step>

  <Step title="確認回傳格式符合預期">
    檢查工具回傳的資料結構是否與 server 文件一致，排除 schema 版本不符。
  </Step>

  <Step title="觀察 OAuth 認證流程">
    HTTP server 常需要首次 OAuth 授權，確認 scope 與你實際需要的功能相符，不要照單全收。
  </Step>
</Steps>

<Warning>
  **常見設定錯誤**

  * **路徑不存在**：`command` 寫絕對路徑但檔案不在；用 `which <command>` 先確認。
  * **`type` 命名錯**：HTTP transport 官方接受 `http` 與 `streamable-http` 兩個值（後者是 MCP 規格名稱），不要寫成 `https` \[1]。
  * **環境變數沒設**：`${API_KEY}` 沒值，啟動時 fail。
  * **多 server 撞 port**：兩個 server 硬編同一 port，後啟動的 fail。
</Warning>

## 3. 選 server：信任、權限、是否官方

引入一個 MCP server 前，過這三題：

### 3.1 信任來源三級

| 等級 | 來源                                                                                                      | 處置                                   |
| -- | ------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| 高  | Anthropic 官方策展的 `claude-plugins-official` 市集內附帶的 MCP server；或工具原廠（GitHub、Notion、Atlassian、Stripe 等）自家發布 | 仍要 review `.mcp.json` 內容與 server 程式碼 |
| 中  | 知名開源 repo，有 issue tracker、有 release、有社群維護                                                               | 額外掃 `rg` 供應鏈指紋（見第 5 節）               |
| 低  | 私人 URL、fork 很久沒更新、單人維護                                                                                  | 預設不安裝；要裝先讀完源碼                        |

### 3.2 權限範圍

server 在 `.mcp.json` 宣稱的 `env`、`args` 暗示它要哪些資源。把它對應到「這個 server 真的需要這個嗎？」：

* 一個只做 `grep` 的 server 不需要 write 權限。
* 一個只查 issue 的 server 不需要對外網路。
* 一個 server 同時要 write + 對外網路 + `~/.ssh` 路徑，最高警覺。

官方 `code.claude.com/docs/en/permissions` 提供 `Bash` / `Edit` / `Read` 的白名單 / 黑名單 / `ask` 三層規則；對 MCP server 的權限收斂走 `mcp__<server>__*` 的 tool 級規則 \[2]。

<Note>
  **權限審查範例**

  一個 server 的 `.mcp.json` 設定：

  ```json theme={null}
  {
    "mcpServers": {
      "slack": {
        "type": "http",
        "url": "https://mcp.slack.com/mcp",
        "oauth": {
          "scopes": "channels:read chat:write search:read"
        }
      }
    }
  }
  ```

  審查點：這個 server 是 Slack 官方嗎？OAuth scope 列了 `chat:write`（可發訊息），你工作流真的需要從 agent 自動發訊息嗎？若不需要，把 `chat:write` 從 scope 拿掉，**只在需要時再放回** \[1]。`oauth.scopes` 讓你把 OAuth 權限收斂到最小（精準 scope 鎖定）。
</Note>

### 3.3 Plugin 提供的 MCP server

Plugin 可以把 MCP server 打包進去 \[1]。`claude-plugins-official` 與 `claude-community` 兩個市集上的 plugin，啟用時其內含的 MCP server 會自動連線。**同樣要 review**：plugin 內的 `.mcp.json` 或 `plugin.json` 的 `mcpServers` 區段，是看 server 設定的地方。

<Tip>
  **plugin MCP server 預設跟手動 server 共用 tool 命名空間**

  啟用 plugin 後，它的 MCP 工具會跟手動配置的 MCP 工具一起出現在 `/mcp` 列表。要區分來源，看 `/mcp` 介面上的 plugin 標示 \[1]。
</Tip>

## 4. 與 CLI、內建工具的取捨

### 4.1 判準

| 情境                                                     | 優先選擇                                                              |
| ------------------------------------------------------ | ----------------------------------------------------------------- |
| 該操作有完整 CLI（`git`、`gh`、`docker`、`kubectl`、`psql` 等）     | CLI（見 [04-10 CLI 優先](/code-agent/customization/cli-first-vs-mcp)） |
| 該操作 Claude.ai / Claude Code 內建就有（Web search、Read、Edit） | 內建                                                                |
| 該服務只提供 REST / GraphQL API，沒有合適 CLI                     | MCP（包成 server）或 WebFetch                                          |
| 該服務已有官方 MCP server（如 GitHub、Notion、Atlassian、Stripe）   | MCP（最常見正解）                                                        |
| 同一操作要跨多個 client（Claude Code + Cursor + Codex）共用        | MCP（跨工具標準的價值所在）                                                   |

### 4.2 不適合 MCP 的場景

* **一次性命令**：用 Bash tool 跑 `gh pr list` 就好，沒必要裝 GitHub MCP server。
* **重型 shell pipeline**：`find | xargs grep | sed | jq` 之類的多步管道，shell 處理最直接；包成 MCP 反而要拆成多次工具呼叫，每次呼叫都吃 context。
* **需要高度可審計的動作**：CLI 指令進 shell history；MCP 呼叫要額外日誌設定。

### 4.3 同一操作不混用 CLI 與 MCP

混用是隱性 bug 來源：你在 shell 跑 `gh pr create`，同一時間 agent 用 MCP 走 `mcp__github__create_pull_request`，兩個都成功了，狀態就不一致。**一個操作選定一條路，fallback 切換後不回頭混用**（[04-10](/code-agent/customization/cli-first-vs-mcp) 詳述）。

## 5. 安全邊界

MCP 是高槓桿整合介面，也是高槓桿攻擊面。連結 [03-3 安全、隱私與供應鏈風險](/code-agent/judgment/security-privacy-supply-chain) 與 [04-7 Plugin](/code-agent/customization/plugins) 的供應鏈 SOP，這裡只列 MCP 特有的：

### 5.1 攻擊面

| 攻擊面                             | 描述                                                                                                               |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| repo 帶入的 `.mcp.json`            | 專案層 server 走 `.mcp.json`，clone 陌生 repo 後 server 可能自動掛上。project scope 需 user approval，但使用者若未仔細閱讀確認，惡意 server 就已啟用 |
| tool 回傳值注入                      | 外部 server 回傳的字串直接進 context，是 prompt injection 路徑                                                                 |
| `enableAllProjectMcpServers` 設定 | 全開，預設拒絕                                                                                                          |
| `headersHelper` 任意指令執行          | v2.1.64+ 引入，**project / local scope 時仍會被執行**。project scope 信任對話框通過後就會跑 \[1]                                      |
| `oauth.scopes` 過寬               | server 申請超過實際需要的 scope                                                                                           |

### 5.2 最小必要防護

```bash theme={null}
# 1. 裝前掃：可疑外連指令
rg -n 'curl|wget|nc|ssh|ANTHROPIC_BASE_URL|enableAllProjectMcpServers' ~/.claude/ .mcp.json

# 2. 隱藏 Unicode 與 bidi 覆寫
rg -nP '[\x{200B}-\x{200D}\x{202A}-\x{202E}]' ~/.claude/ .mcp.json

# 3. HTML 註解、script tag、base64
rg -n '<!--|<script|data:text/html|base64,' ~/.claude/ .mcp.json
```

加上 [03-3](/code-agent/judgment/security-privacy-supply-chain) 通用 SOP：敏感路徑（`~/.ssh`、`~/.aws`、`**/.env*`）在 `permissions.deny` 阻擋；MCP 工具呼叫要有日誌；server 啟用清單每季 review 一次。

### 5.3 Tool search 與 context 控制

Claude Code 預設開啟 MCP tool search：MCP tools 不會在 session 啟動時全量載入 context，而是 Claude 透過 `ToolSearch` 動態發現 \[1]。這大幅降低多 MCP server 的 context 成本，但**不是安全機制**。已連線的 server 仍可能因任務相關被叫到。對特別敏感的 server，可設 `alwaysLoad: false`（預設）外加在 `permissions.deny` 設 `mcp__<server>__*` 阻擋。

## 6. 工具對照

<ToolCompare
  lang="zh"
  tools={[
{ id: "claude", label: "Claude Code" },
{ id: "codex",  label: "OpenAI Codex" },
{ id: "gemini", label: "Google Gemini CLI" },
{ id: "copilot",label: "GitHub Copilot" },
{ id: "cursor", label: "Cursor" },
]}
  dimensions={[
{
  id: "proj-config",
  label: "設定檔（專案層）",
  cells: {
    claude:  { value: ".mcp.json",                                  recommend: true, detail: "進版控，team 共享；啟動時標為待核准，需互動式 review（CVE-2025-59536 後的防護）。" },
    codex:   { value: ".codex/config.toml", detail: "Codex CLI 的 MCP 設定放在 config.toml 的 [mcp] 區段，trusted projects only（截至 2026-06，依官方 MCP 章節 [5]）。" },
    gemini:  { value: ".gemini/settings.json", detail: "mcpServers 區段設定 MCP server，scope 預設 project（截至 2026-06，依官方 Gemini CLI 文件 [6]）。" },
    copilot: { value: "無專案層設定檔", detail: "GitHub Copilot CLI 的 MCP 設定集中在使用者層 ~/.copilot/mcp-config.json，無對等專案層設定檔（截至 2026-06，依官方文件 [7]）。" },
    cursor:  { value: ".cursor/mcp.json", detail: "Cursor 以 .cursor/mcp.json 作為專案層 MCP 設定，優先序高於使用者層（截至 2026-06，依官方 MCP 文件 [8]）。" },
  },
},
{
  id: "user-config",
  label: "設定檔（使用者層）",
  cells: {
    claude:  { value: "~/.claude.json",                             recommend: true },
    codex:   { value: "~/.codex/config.toml", detail: "使用者層 MCP 設定與其他 Codex 設定共用同一 config.toml（截至 2026-06，依官方設定參考 [5]）。" },
    gemini:  { value: "~/.gemini/settings.json", detail: "gemini mcp add --scope user 寫入使用者層 settings.json（截至 2026-06，依官方 Gemini CLI 文件 [6]）。" },
    copilot: { value: "~/.copilot/mcp-config.json", detail: "GitHub Copilot CLI 唯一的 MCP 設定檔，跨所有 session 與 workspace 生效（截至 2026-06，依官方文件 [7]）。" },
    cursor:  { value: "~/.cursor/mcp.json", detail: "全域設定，適用所有專案；專案層 .cursor/mcp.json 可覆寫（截至 2026-06，依官方 MCP 文件 [8]）。" },
  },
},
{
  id: "stdio",
  label: "支援 stdio",
  cells: {
    claude:  { value: "是 [1]",                                     recommend: true },
    codex:   { value: "是", detail: "Codex CLI 支援 stdio transport（本機行程 stdin/stdout 通訊）（截至 2026-06，依官方 MCP 章節 [5]）。" },
    gemini:  { value: "是", detail: "Gemini CLI 以 command 欄位設定 stdio server，spawn subprocess 通訊（截至 2026-06，依官方 Gemini CLI 文件 [6]）。" },
    copilot: { value: "是", detail: "GitHub Copilot CLI 支援 stdio transport，推薦作為與 VS Code 等 MCP client 相容的標準格式（截至 2026-06，依官方文件 [7]）。" },
    cursor:  { value: "是", detail: "Cursor 內建 MCP client，stdio 為最常見的本機 server 連線方式（截至 2026-06，依官方 MCP 文件 [8]）。" },
  },
},
{
  id: "http",
  label: "支援 http（SSE 替代）",
  cells: {
    claude:  { value: "是（streamable-http 別名）[1]",              recommend: true, detail: "官方同時接受 http 與 streamable-http 兩個 type 值；SSE 已標為 deprecated，新設定用 http。" },
    codex:   { value: "是（Streamable HTTP）", detail: "Codex CLI 支援 Streamable HTTP server，以 url 欄位設定遠端地址；同時支援 bearer token 與 OAuth（截至 2026-06，依官方 MCP 章節 [5]）。" },
    gemini:  { value: "是（httpUrl 欄位）", detail: "Gemini CLI 以 httpUrl 欄位設定 HTTP streaming transport，--transport http 旗標可指定（截至 2026-06，依官方 Gemini CLI 文件 [6]）。" },
    copilot: { value: "是（Streamable HTTP）", detail: "GitHub Copilot CLI 支援 HTTP Streamable transport；SSE 仍相容但已 deprecated（截至 2026-06，依官方文件 [7]）。" },
    cursor:  { value: "是（Streamable HTTP）", detail: "Cursor 支援 Streamable HTTP，適合遠端或 team 環境的 MCP server（截至 2026-06，依官方 MCP 文件 [8]）。" },
  },
},
{
  id: "auto-approval",
  label: "自動核准機制（安全風險）",
  cells: {
    claude:  { value: "project scope 預設待核准 [1]",               recommend: true, detail: "CVE-2025-59536 後加的防護：.mcp.json 的 server 啟動時標為待核准，不能不知情自動掛上。" },
    codex:   { value: "trusted projects 設計", detail: "Codex 的專案層設定需透過 trusted projects 機制確認，未信任的專案不自動套用。" },
    gemini:  { value: "無獨立核准流程", detail: "Gemini CLI 無類似 project scope 待核准機制；server 在 settings.json 宣告後直接生效（截至 2026-06）。" },
    copilot: { value: "無獨立核准流程", detail: "GitHub Copilot CLI MCP server 由使用者手動在 mcp-config.json 設定，無自動待核准機制（截至 2026-06）。" },
    cursor:  { value: "無獨立核准流程", detail: "Cursor 讀取 .cursor/mcp.json 或 ~/.cursor/mcp.json 直接啟用，無 pending approval 機制（截至 2026-06）。" },
  },
},
{
  id: "plugin-mcp",
  label: "Plugin 內 MCP",
  cells: {
    claude:  { value: "支援 [1]",                                   recommend: true, detail: "Plugin 可打包 MCP server；啟用後工具進 mcp__<server>__<tool> 命名空間，與手動設定的 server 共用列表。" },
    codex:   { na: true },
    gemini:  { na: true },
    copilot: { value: "透過 Extensions", detail: "GitHub Copilot 的 Extensions 機制可提供額外工具，功能上類似 Plugin 內 MCP，但架構不同（截至 2026-06）。" },
    cursor:  { na: true },
  },
},
{
  id: "official-dir",
  label: "官方 server 目錄",
  cells: {
    claude:  { value: "claude.ai/directory [1]",                    recommend: true },
    codex:   { value: "無官方目錄", detail: "OpenAI Codex 文件提供幾個常見 server 範例（Context7、Figma、Playwright 等），但無集中式官方目錄（截至 2026-06，依官方 MCP 章節 [5]）。" },
    gemini:  { value: "無官方目錄", detail: "Gemini CLI 文件以範例形式介紹 GitHub MCP server 等，無集中式官方目錄（截至 2026-06，依官方文件 [6]）。" },
    copilot: { value: "GitHub MCP Registry（github.com/mcp）", detail: "GitHub 提供官方 MCP Registry，列出安裝說明、可用工具與各 server URL（截至 2026-06，依官方文件 [7]）。" },
    cursor:  { value: "無官方目錄", detail: "Cursor 無集中式官方 MCP server 目錄；使用者自行查找並設定（截至 2026-06，依官方 MCP 文件 [8]）。" },
  },
},
{
  id: "tool-search",
  label: "Tool search 預設",
  cells: {
    claude:  { value: "開啟 [1]",                                   recommend: true, detail: "MCP tools 不在 session 啟動時全量載入，而是 Claude 透過 ToolSearch 動態發現，降低多 server 的 context 成本。" },
    codex:   { value: "無對等機制", detail: "Codex CLI 無 MCP tool search 機制；連線 server 後工具直接可用（截至 2026-06）。" },
    gemini:  { value: "無對等機制", detail: "Gemini CLI 無 MCP tool search 機制（截至 2026-06）。" },
    copilot: { value: "無對等機制", detail: "GitHub Copilot CLI 無 MCP tool search 機制（截至 2026-06）。" },
    cursor:  { value: "無對等機制", detail: "Cursor 無 MCP tool search 機制（截至 2026-06）。" },
  },
},
{
  id: "oauth",
  label: "OAuth 支援",
  cells: {
    claude:  { value: "是（HTTP）[1]",                              recommend: true, detail: "oauth.scopes 讓你把 OAuth 權限收斂到最小；HTTP server 走 OAuth，stdio server 透過 env 傳 key。" },
    codex:   { value: "是（OAuth + bearer token）", detail: "Codex CLI Streamable HTTP server 支援 OAuth 與 bearer token 認證（截至 2026-06，依官方 MCP 章節 [5]）。" },
    gemini:  { value: "是（HTTP transport）", detail: "Gemini CLI 的 HTTP streaming transport 支援 OAuth 認證（截至 2026-06，依官方文件 [6]）。" },
    copilot: { value: "是（HTTP transport）", detail: "GitHub Copilot CLI HTTP transport 支援 OAuth（截至 2026-06，依官方文件 [7]）。" },
    cursor:  { value: "是（HTTP transport）", detail: "Cursor 支援 HTTP transport 的 OAuth 認證（截至 2026-06，依官方 MCP 文件 [8]）。" },
  },
},
]}
  notes={[
"Claude Code 的 project scope MCP server 啟動時需互動式核准（CVE-2025-59536 防護），其他工具無此機制。",
"GitHub Copilot CLI 無專案層 MCP 設定檔，所有 server 集中在使用者層 ~/.copilot/mcp-config.json。",
"Cursor 為第三方 IDE（Anysphere），本 Playbook 僅短提一欄。",
]}
/>

<Note>
  **命名與邊界**

  * **Claude Code 內建 MCP client**；不需要裝 plugin 就能用 MCP server。
  * **OpenAI、Antigravity、GitHub Copilot、Cursor** 對 MCP 的支援深度與時程各異，採用前以各家當前官方文件為準。
  * **SSE transport 已在官方文件標為 deprecated**；新設定用 `http` \[1]。
</Note>

## 動手做

<Note>
  **30 分鐘練習**

  1. **接一個 server 跑通**（15 分鐘）：裝 `claude mcp add --transport http notion https://mcp.notion.com/mcp`（或選你已有的帳號，如 Sentry、GitHub）。在 Claude Code 內 `/mcp` 確認狀態，跑一次簡單呼叫驗證連線。
  2. **寫 project scope 設定**（10 分鐘）：把你目前用 MCP 完成的一個操作，搬到 `.mcp.json` 走 project scope；下次啟動 Claude Code 觀察 user approval 對話框，決定該批准還是拒絕。
  3. **安全掃描**（5 分鐘）：對 `~/.claude.json` 與 `.mcp.json` 跑本節的 `rg` 三條掃描，把所有 hit 記下來並人工 review。
</Note>

## 常見誤區

<Warning>
  **反模式清單**

  * **直接信任 repo 帶入的 `.mcp.json` 而不審查**：等同讓外部程式碼決定 agent 的工具集。clone 陌生 repo 後第一件事：開 `.mcp.json` 看看有什麼。
  * **給 MCP server 過寬權限**（write + 對外網路 + `~/.ssh` 路徑），遠超其實際功能所需。**先裝最窄 scope**；需要時再加。
  * **同一操作 CLI 與 MCP 混用**：導致結果從兩個來源拼接、狀態不一致。**一個操作一條路**（[04-10](/code-agent/customization/cli-first-vs-mcp) 詳述）。
  * **裝了一堆 server 但從未 review**：`/mcp` 列出十幾個，但有幾個你忘了為什麼裝。**每季掃一次**，把不用的 `mcp remove` 掉。
  * **把 OAuth scope 全開**：server 申請什麼 scope 就接受什麼。**預設拒絕**；需要的功能才放 scope。
  * **把 MCP 當作「萬靈丹」**：MCP 解決「沒 CLI 的服務的整合」。**有 CLI 的場景應優先 CLI**（[04-10](/code-agent/customization/cli-first-vs-mcp)）。
</Warning>

## 自我檢核

<Check>
  **通過本單元的標準**

  1. 你能在一分鐘內說出 MCP 三種 scope 的差異，以及哪一種會被 user approval 攔下？
  2. 你能列出至少三條判斷「是否該裝某個 MCP server」的問題？
  3. 你目前主用的 MCP server（或考慮裝的），你能說出它的信任來源等級、它宣稱的權限範圍、以及你驗證過它確實只用那些權限嗎？
  4. 你能在裝一個新 MCP server 之前，跑完三條 `rg` 供應鏈掃描嗎？
</Check>

## 來源與延伸閱讀

事實主張依官方文件，快變動項標註截至 2026-05。

<div className="references">
  * \[1] Anthropic, "Connect Claude Code to tools via MCP," code.claude.com, 2026. \[Online]. Available: [https://code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp) （截至 2026-06；含四種 transport、三種 scope、OAuth、`headersHelper`、tool search、`streamable-http` 別名、安全提示與 `claude.ai/directory` 官方 server 目錄）

  * \[2] Anthropic, "Configure permissions," code.claude.com, 2026. \[Online]. Available: [https://code.claude.com/docs/en/permissions](https://code.claude.com/docs/en/permissions) （截至 2026-06；MCP tool 級 `mcp__server__*` 白名單 / 黑名單規則、與 `Bash` / `Edit` 規則的搭配）

  * \[3] Model Context Protocol, "MCP specification," modelcontextprotocol.io, 2026. \[Online]. Available: [https://modelcontextprotocol.io](https://modelcontextprotocol.io) （截至 2026-06；官方規格首頁）

  * \[4] Snyk, "ToxicSkills: 2026 Report on Malicious Skills in the Wild," snyk.io, 2026. \[Online]. Available: [https://snyk.io](https://snyk.io) （截至 2026-06；含 Skill / MCP server 供應鏈風險統計）

  * \[5] OpenAI, "Model Context Protocol," developers.openai.com, 2026. \[Online]. Available: [https://developers.openai.com/codex/mcp](https://developers.openai.com/codex/mcp) （截至 2026-06；Codex CLI MCP config.toml 位置、stdio 與 Streamable HTTP transport、OAuth 與 bearer token 認證）

  * \[6] Google, "MCP servers with Gemini CLI," google-gemini.github.io, 2026. \[Online]. Available: [https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html) （截至 2026-06；settings.json 路徑與 mcpServers 區段、stdio / SSE / HTTP streaming 三種 transport）

  * \[7] GitHub Docs, "Adding MCP servers for GitHub Copilot CLI," docs.github.com, 2026. \[Online]. Available: [https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers) （截至 2026-06；\~/.copilot/mcp-config.json、stdio / HTTP / SSE transport、GitHub MCP Registry）

  * \[8] Cursor, "MCP," cursor.com, 2026. \[Online]. Available: [https://cursor.com/docs/cli/mcp](https://cursor.com/docs/cli/mcp) （截至 2026-06；.cursor/mcp.json 與 \~/.cursor/mcp.json 位置、stdio 與 Streamable HTTP transport）
</div>

* 安全面見 [03-3 安全、隱私與供應鏈風險](/code-agent/judgment/security-privacy-supply-chain)。
* CLI 優先心法見 [04-10 CLI 優先](/code-agent/customization/cli-first-vs-mcp)。
* 內建工具盤點見 [04-8 各家 AI 的內建工具與功能](/code-agent/customization/vendor-builtin-tools)。
* Plugin 內 MCP server 見 [04-7 Plugin](/code-agent/customization/plugins)。
* Subagent 透過 `mcpServers` 預載 server 見 [04-5 Subagent](/code-agent/customization/subagents)。
* Hook 強制執行見 [04-6 Hooks](/code-agent/customization/hooks)。
