> ## 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-10 CLI 優先：CLI 與 MCP 的差異，為何優先選 CLI

> 能用 CLI 完成的事，優先用 CLI；MCP 是 fallback，不是預設。本單元講 CLI 與 MCP 的本質差異：token 成本、可組合性、可審計性、認證與狀態管理，並給出何時才退回 MCP 的明確判準。

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={[
"能用 CLI 完成的優先 CLI",
"一個操作選定一條路，CLI 跟 MCP 結果是兩回事",
"MCP 不是 AI-native 就更現代，是更貴的解法",
"gh auth 一次永久，OAuth 流程每次過期重來",
"同時跑 gh pr create 跟 mcp__github__create_pull_request",
"shell history 明文可審計，MCP 呼叫要額外日誌",
"fallback 切換沒在 CLAUDE.md 標明，下個 session 又是新猜測",
]}
/>

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

  能用 CLI 完成的事，優先用 CLI；MCP 是 fallback，不是預設。這個單元講 CLI 與 MCP 的本質差異：token 成本、可組合性、可審計性、認證與狀態管理，解釋為何 CLI-first 是更穩健的預設策略，並給出「什麼條件才退回 MCP」的明確判準。最後用同一個工作流的 CLI vs MCP 對照 worked example 把差異量化。
</Info>

## 學習目標

* [ ] 講清 CLI 與 MCP 在 token 成本、可組合性、可審計性、認證與狀態管理上的本質差異。
* [ ] 說出 CLI-first 策略的理由，並舉出具體的可組合性優勢（pipe、`--json`、`jq` 串接）。
* [ ] 判斷何時應 fallback 到 MCP（CLI 不可用、未認證、rate limit、無 CLI 對應）。
* [ ] 避免同一操作同時走 CLI 與 MCP，避免結果分裂與 token 浪費。

***

## 1. CLI 與 MCP 各是什麼

**CLI**（Command Line Interface）是既有的命令列工具：`git`、`gh`、`docker`、`kubectl`、`psql`、`aws`、`npm`、`uv`、`make`、`jq` 等。呼叫後直接輸出到 stdout / stderr，不需要額外協定層。

**MCP**（Model Context Protocol）是 2024 年由 Anthropic 提出的開放標準，定義「外部工具與資料源如何被 LLM 結構化地呼叫」\[1]。MCP server 對外暴露 `tools` / `resources` / `prompts` 三類介面；MCP client（如 Claude Code、Cursor）連上後把工具掛入自己的 `mcp__<server>__<tool>` 命名空間。

兩者的角色定位差異：

|       | CLI                        | MCP                                       |
| ----- | -------------------------- | ----------------------------------------- |
| 本質    | 工具本身                       | 工具接入的中間層協定                                |
| 主要消費者 | 人（鍵盤）+ agent（透過 Bash tool） | agent（MCP client）                         |
| 互通性   | 跨工具、跨語言、跨作業系統              | 跨支援 MCP 的 client，但 server / client 矩陣仍在收斂 |
| 成熟度   | 數十年                        | 兩年多，仍在規格演進                                |

<Tip>
  **Claude Code 對 Bash tool 的設計**

  Claude Code 把 Bash 列為核心工具類型之一（與檔案修改、唯讀並列三層）\[2]。`ls`、`cat`、`grep`、`find`、`wc`、`which`、`git status` 等唯讀指令內建免 prompt \[2]；其他指令走 `Bash(specifier)` 形式的 allow / deny / ask 規則。這個設計本身就把 Bash 放在最常用、最高優先的位置。
</Tip>

## 2. 本質差異

### 2.1 Token 成本

**MCP 的固定 context 成本**：MCP server 連線後，其工具描述（tool schema）會進入模型 context。即便 tool search 預設開啟讓 schema 延後載入，已被 Claude 發現的 tool 仍佔用對話輪次的 context \[1]。

**CLI 的可變 context 成本**：CLI 沒有預載的 schema。`Bash` tool 的 schema 是固定的、不隨你跑了什麼指令而變大。真正的成本只在 stdout 進入 context 的那一刻，而 stdout 是你可控的（用 `head`、`tail`、`jq -c '.[0]'` 預先裁剪）。

實測估算（粗略，量級用）：一個簡單 MCP 工具呼叫的單次 token 成本包含「schema 觸發 + 結構化 JSON 回應」兩個部分；同等的 CLI 呼叫，schema 成本是 0，回應可以裁到只剩一行 JSON。對 200k 脈絡視窗，差距看起來小；對一天跑幾十次的工作流，每月差距是數萬 token 級別。

### 2.2 可組合性

**CLI 是 shell-native**：每個 CLI 的輸出可被另一個 CLI 透過 pipe 串接，形成高密度資訊萃取。

```bash theme={null}
# 找出本月（createdAt 在 2026-05 之後）review 數超過 5 的 PR
gh pr list --state all --json number,createdAt \
  | jq -r '.[] | select(.createdAt > "2026-05-01") | .number' \
  | xargs -I{} gh pr view {} --json number,reviews \
  | jq -r 'select((.reviews | length) > 5) | .number'
```

**MCP 沒有等價的 shell-level 串接**：每次 `mcp__<server>__<tool>` 呼叫獨立，輸出回 context 後 Claude 決定下一步。一個跨四步的 pipeline 會吃四次 schema 觸發、四次完整回應。

### 2.3 可審計性

**CLI 指令明文可見、可記錄於 shell history**。`auditd`、`syscall`、bash history 都能稽核。CI 環境下 git log 內的 `run:` 段也是確定性紀錄。

**MCP 呼叫在 agent 執行層**，需要額外日誌機制（Hook、Subagent log）才可追蹤（[04-6 Hooks](/code-agent/customization/hooks) 提供部分支援，但不是預設）。對合規需求高的場景（金融、醫療、學術審查），CLI 的可審計性是結構性優勢。

### 2.4 認證與狀態管理

**CLI 沿用作業系統層級認證**：`gh auth login`、`aws configure`、`gcloud auth login` 把 token 存進 OS keychain 或 `~/.config` 對應目錄；後續指令自動帶認證。Claude Code 的 Bash tool 對認證檔的讀取權限可由 `permissions.deny` 細部管控 \[2]。

**MCP 認證是各 server 自己的事**：HTTP server 走 OAuth（[04-9 MCP 整合](/code-agent/customization/mcp-integration) 詳述）；stdio server 透過 `env` 傳 key；多數 server 沒有 session 狀態，但部分（如 Notion 編輯、Atlassian 變更）有。狀態管理更複雜，需要個別 server 理解。

## 3. 為何 CLI-first

把上面的差異收斂成四個具體理由：

1. **CLI 是該生態的官方正規用法**。`gh` 是 GitHub 官方 CLI、`kubectl` 是 K8s 官方 CLI、`aws` 是 AWS 官方 CLI。文件完整、版本穩定、社群覆蓋廣、與上游版本同步。繞過它等於繞過一條已經有人維護的正路。
2. **輸出格式可控**。`gh pr list --json number,title,state` 精準到欄位；MCP 呼叫回傳整個結構化物件，你要的欄位與不要的欄位都進 context。
3. **不額外吃 context**。Bash tool schema 是固定的；MCP 工具描述隨 server 數量線性增加。
4. **除錯路徑清晰**。CLI 失敗有明確 exit code 與 stderr；MCP 失敗的診斷路徑因 server 而異（協議錯誤、認證過期、工具不存在都不同）。

<Note>
  **沒有官方明文「CLI first」指引**

  截至 2026-06，Anthropic 官方文件並未在 \[permissions] 與 \[MCP] 章節使用「CLI first」字樣 \[1, 2]。「CLI 優先」是本 Playbook 從官方架構推導出來的實務策略：Bash 是核心工具類型、唯讀指令免 prompt、權限精細到指令前綴。這些設計本身就把 Bash 放在最高槓桿位置，CLI-first 是順著這條路走。
</Note>

## 4. 何時用 MCP（fallback 觸發條件）

CLI-first 是預設。只有滿足下列任一條件才切換到 MCP：

| 觸發條件                   | 範例                                                |
| ---------------------- | ------------------------------------------------- |
| 該服務根本沒有 CLI            | 多數 SaaS（Notion 早期、Linear、Sentry 部分功能）只提供 REST API |
| CLI 需認證，但 agent 環境無法完成 | OAuth 流程必須互動、不能用 device code flow                 |
| CLI 已達 rate limit      | 多個 client 共用同一組 token；MCP server 可有獨立的 token pool |
| 需要跨多個 MCP client 統一    | 同一個 server 給 Claude Code 與 Cursor 共用，避免在兩處各寫一份    |
| 該操作需要結構化、型別化的回應        | schema 驗證是 MCP 設計目標之一                             |

<Warning>
  **不是「MCP 也可以，兩個都試試」**

  fallback 觸發規則：任一條件明確成立才切換。不要「看起來 MCP 也行、就裝一個」，多裝一個 server 永遠有 context 成本、權限面、供應鏈風險。一個工作流選定一條路，切換後不回頭混用（下節）。
</Warning>

## 5. Worked example：同一操作走 CLI 與走 MCP

### 5.1 情境

查詢某 GitHub repo 的最新 10 筆 open PR，列出編號、標題、作者、是否為 draft。

### 5.2 CLI 路徑

```bash theme={null}
gh pr list --repo owner/repo --state open --limit 10 \
  --json number,title,author,isDraft \
  | jq -r '.[] | "\(.number)\t\(.title)\t\(.author.login)\tdraft=\(.isDraft)"'
```

特性：

* stdout 一次輸出，可直接 `| head`、`| wc -l`、`| column -t` 後處理。
* 失敗時 exit code 非 0，stderr 明確（「API rate limit exceeded」、「auth required」）。
* token 成本：0 schema 載入 + 約 800 token 結構化輸出（10 筆 PR 的 JSON）。
* 對 Claude 而言，Bash tool 的 schema 早已常駐，不會因這次呼叫而增加。

### 5.3 MCP 路徑

假設已裝 GitHub 官方 MCP server（`mcp__github__list_pull_requests`）：

```text theme={null}
呼叫 mcp__github__list_pull_requests(owner='owner', repo='repo', state='open', per_page=10)，回傳後摘要
```

特性：

* tool schema 進入 context（tool search 預設開啟所以是觸發時載入，但載入就要佔 context）。
* 回傳是完整結構化物件（可能含 `head`、`base`、`user` 等你用不到的欄位），通常 1,500-3,000 token。
* 你要「draft 狀態」這種衍生欄位，要 Claude 在 context 內再運算一次。
* 失敗時的錯誤訊息因 server 設計而異，部分 server 對人類不友善。

### 5.4 對照重點

| 面向          | CLI（`gh`）                          | MCP（`mcp__github__*`）  |
| ----------- | ---------------------------------- | ---------------------- |
| Schema 載入成本 | 0（共用 Bash tool schema）             | 每次 server 連線觸發時載入      |
| 單次回應 token  | 約 800（裁切後可更低）                      | 約 1,500-3,000（完整物件）    |
| 可組合性        | 可 pipe / `jq` / `xargs` / 進檔案      | 須 Claude 在 context 內運算 |
| 可審計性        | shell history / git log / `auditd` | 需 Hook 或 server log    |
| 除錯          | 明確 exit code + stderr              | 依 server 實作            |
| 認證          | `gh auth login` 一次永久               | OAuth 流程每次 token 過期重來  |

<Tip>
  **量化評估**

  假設一天跑這個查詢 50 次：CLI 路徑每月多燒約 0.6M token（回應部分）；MCP 路徑每月多燒約 2.3M token（schema + 回應）。在 Claude Sonnet 4.6 級別的定價下，每月差距是數美元級。但疊上多個 server 後，差距是數十美元級。CLI-first 不只是省 token，也是省 context 預算給真正重要的事。
</Tip>

## 6. 反模式：CLI 與 MCP 混用

最常見的隱性 bug 來源。具體場景：

```bash theme={null}
# 你手動在 shell 跑：建立 PR
gh pr create --base main --head feature-x --title "WIP" --body "..."
```

同一時間，agent 接到「幫我建立 PR」指令，不知道你手動建了，於是又用 MCP 走：

```text theme={null}
mcp__github__create_pull_request(...)
```

結果：兩個 PR 並存、訊息不一致、`gh` 端的 CI 跑了一次、MCP 端的 CI 又跑了一次。狀態分裂。

規則：

1. **一個操作選定一條路**：今天用 CLI 就 CLI、用 MCP 就 MCP，整個 session 一致。
2. **fallback 切換後不回頭**：CLI 不可用才切 MCP，不要「兩邊都試看看哪個行」。
3. **跨 session 也要標明**：在 `CLAUDE.md` 寫「本專案統一用 `gh` CLI，不裝 GitHub MCP server」，給 session 一致的預設。

## 7. 工具對照

<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 CLI" },
{ id: "cursor",  label: "Cursor" },
]}
  dimensions={[
{
  id: "official-cli",
  label: "官方 CLI",
  cells: {
    claude:  { value: "`claude` CLI（Claude Code）[1]", recommend: true },
    codex:   { value: "`codex` CLI", detail: "OpenAI Codex CLI，以 `codex` 指令呼叫，支援互動與非互動模式（截至 2026-06，依官方 Codex CLI 文件 [3]）。" },
    gemini:  { value: "`gemini` CLI", detail: "Google Gemini CLI，以 `gemini` 指令呼叫，支援互動模式與 --prompt 非互動模式（截至 2026-06，依官方 Gemini CLI 文件 [4]）。" },
    copilot: { value: "`gh copilot` 子命令", detail: "GitHub Copilot CLI 以 `gh extension install github/gh-copilot` 裝為 gh 擴充，主要入口是 `gh copilot suggest` 與 `gh copilot explain`（截至 2026-06，依官方文件 [5]）。" },
    cursor:  { value: "`cursor` CLI", detail: "Cursor IDE 提供 cursor CLI 用於從終端機開啟 IDE，非獨立 agent CLI（截至 2026-06，依官方 MCP 文件 [6]）。" },
  },
},
{
  id: "bash-support",
  label: "Bash 工具支援",
  cells: {
    claude:  { value: "核心工具（內建免 prompt 唯讀指令）[2]", recommend: true, detail: "Bash 是 Claude Code 三大核心工具類型之一；ls、cat、grep 等唯讀指令預設免 approval prompt，其他走 allow/deny/ask 規則。" },
    codex:   { value: "內建 shell 執行", detail: "Codex CLI 內建 shell 執行能力，以沙箱或 Docker 環境執行指令；--approval-mode 控制自動執行程度（截至 2026-06，依官方 Codex CLI 文件 [3]）。" },
    gemini:  { value: "內建 shell 執行", detail: "Gemini CLI 內建 shell 工具，可執行 Bash 指令；以 --sandbox 或 Docker 進行隔離（截至 2026-06，依官方 Gemini CLI 文件 [4]）。" },
    copilot: { value: "透過 gh 子命令", detail: "GitHub Copilot CLI 主要是 suggest/explain 模式，shell 執行由使用者確認後在本機跑；無獨立 Bash tool 層（截至 2026-06，依官方文件 [5]）。" },
    cursor:  { value: "IDE 終端機整合", detail: "Cursor 的 agent 可在整合終端機執行 shell 指令，由 IDE 層管理審核；非獨立 Bash tool 機制（截至 2026-06，依官方 MCP 文件 [6]）。" },
  },
},
{
  id: "mcp-support",
  label: "MCP 支援",
  cells: {
    claude:  { value: "內建 MCP client，`.mcp.json` 設定 [1]", recommend: true },
    codex:   { value: "支援（config.toml [mcp] 區段）", detail: "Codex CLI 支援 stdio 與 Streamable HTTP transport，設定放 .codex/config.toml 的 [mcp] 區段（截至 2026-06，依官方 MCP 章節 [3]）。" },
    gemini:  { value: "支援（settings.json mcpServers 區段）", detail: "Gemini CLI 支援 stdio、SSE、HTTP streaming，設定放 .gemini/settings.json 的 mcpServers 區段（截至 2026-06，依官方文件 [4]）。" },
    copilot: { value: "支援（~/.copilot/mcp-config.json）", detail: "GitHub Copilot CLI 支援 stdio 與 HTTP transport，設定集中在使用者層 ~/.copilot/mcp-config.json，無專案層設定檔（截至 2026-06，依官方文件 [5]）。" },
    cursor:  { value: "內建（.cursor/mcp.json）", detail: "Cursor 內建 MCP client，以 .cursor/mcp.json（專案）與 ~/.cursor/mcp.json（使用者）設定，支援 stdio 與 Streamable HTTP（截至 2026-06，依官方 MCP 文件 [6]）。" },
  },
},
{
  id: "auth",
  label: "認證管理",
  cells: {
    claude:  { value: "OS keychain + `ANTHROPIC_API_KEY` 環境變數 + 子 CLI 自帶", recommend: true },
    codex:   { value: "OPENAI_API_KEY 環境變數 + OAuth", detail: "Codex CLI 以 OPENAI_API_KEY 或 OAuth 認證；MCP HTTP server 另支援 bearer token（截至 2026-06，依官方文件 [3]）。" },
    gemini:  { value: "GEMINI_API_KEY 環境變數 + gcloud ADC", detail: "Gemini CLI 以 GEMINI_API_KEY 或 Google 帳號 OAuth（Application Default Credentials）認證（截至 2026-06，依官方文件 [4]）。" },
    copilot: { value: "`gh auth login` 一次永久", detail: "GitHub Copilot CLI 沿用 gh CLI 的 OAuth token，gh auth login 後自動帶認證，token 存 OS keychain（截至 2026-06，依官方文件 [5]）。" },
    cursor:  { value: "Cursor 帳號 OAuth + API key 設定", detail: "Cursor 以 Cursor 帳號（OAuth）+ 各模型 API key 管理認證，MCP HTTP server 走 OAuth（截至 2026-06，依官方文件 [6]）。" },
  },
},
{
  id: "token-budget",
  label: "Token 預算控制",
  cells: {
    claude:  { value: "tool search 預設開啟 [1]", recommend: true, detail: "MCP tool search 讓工具描述在需要時才載入 context，降低多 server 的固定 context 成本。" },
    codex:   { value: "context window 自動管理", detail: "Codex CLI 以自動 context 壓縮（summarization）管理長對話；無 MCP tool search 對等機制（截至 2026-06，依官方文件 [3]）。" },
    gemini:  { value: "context window 自動管理", detail: "Gemini CLI 依模型 context window 管理對話長度；無 MCP tool search 對等機制（截至 2026-06，依官方文件 [4]）。" },
    copilot: { value: "無獨立 token 預算機制", detail: "GitHub Copilot CLI 以請求為單位，無跨輪次 token 預算控制機制（截至 2026-06，依官方文件 [5]）。" },
    cursor:  { value: "IDE 層管理（上下文選取）", detail: "Cursor 讓使用者在 IDE 中選取要放入 context 的檔案與片段；無 MCP tool search 對等機制（截至 2026-06，依官方文件 [6]）。" },
  },
},
{
  id: "cli-first-guidance",
  label: "CLI-first 策略官方指引",
  cells: {
    claude:  { value: "隱含（Bash 為核心工具）[2]", recommend: true, detail: "官方文件未明文 CLI-first，但 Bash 列為三大核心工具之一、唯讀指令免 prompt 的設計本身就把 Bash 放在最高槓桿位置。" },
    codex:   { value: "無明文 CLI-first 指引", detail: "Codex CLI 文件以工具能力為主，未就 CLI vs MCP 優先序給明文策略建議（截至 2026-06）。" },
    gemini:  { value: "無明文 CLI-first 指引", detail: "Gemini CLI 文件以能力介紹為主，未就 CLI vs MCP 優先序給明文策略建議（截至 2026-06）。" },
    copilot: { value: "無明文 CLI-first 指引", detail: "GitHub Copilot CLI 文件以 suggest/explain 場景為主，未就 CLI vs MCP 優先序給明文策略建議（截至 2026-06）。" },
    cursor:  { value: "無明文 CLI-first 指引", detail: "Cursor 以 IDE 整合為核心，未就 CLI vs MCP 優先序給明文策略建議（截至 2026-06）。" },
  },
},
]}
  notes={[
"Claude Code 的 Bash tool 是 agent 呼叫 shell 的介面；`claude` CLI 是呼叫 Claude Code 本身的命令，兩者層級不同。",
"GitHub Copilot CLI 以 `gh copilot` 子命令運作，不是獨立的 `copilot` 可執行檔。",
"Cursor 為第三方 IDE（Anysphere），本 Playbook 僅短提一欄。",
]}
/>

<Note>
  **命名澄清**

  Claude Code 的 Bash tool 是 agent 呼叫 shell 的介面；`claude` CLI 是呼叫 Claude Code 本身的命令。兩者層級不同。各工具的 MCP 設定細節見 [04-9 MCP 整合](/code-agent/customization/mcp-integration)。Cursor 為第三方 IDE（Anysphere），本 Playbook 僅短提一欄。
</Note>

## 動手做

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

  1. **建 CLI-first checklist**（10 分鐘）：列出你工作流中常見的 10 個操作（查 issue、查 PR、跑測試、看 log、查 DB、deploy 等），逐一標記「有 CLI 可用」「需 MCP fallback」「內建工具就夠」。這份清單是下個 session 開工的預設。
  2. **同一操作雙路對照**（15 分鐘）：選一個你常用的操作（如查 GitHub issue），先用 CLI 完成（記下 token 估算：用 `wc -w` 或 `wc -c` 大致估算 stdout 大小），再用 MCP 完成（看 context 中實際佔多少 token）。差距量化，決定哪一條作為預設。
  3. **混用偵測**（5 分鐘）：翻 `~/.bash_history` 與最近 session 的 transcript，找出有沒有 CLI 與 MCP 並存呼叫同一服務的痕跡。標記、決定要不要統一。
</Note>

## 常見誤區

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

  * **以為「MCP 是 AI-native 的更現代做法」就預設用 MCP**：忽略 CLI 在可組合性與 token 成本上的結構性優勢。新不等於更適合你的場景。
  * **CLI 可用，但 MCP 設定已存在就懶得切換**：導致 context 浪費。設定存在不構成預設，每次新操作都過 fallback 觸發條件。
  * **同一 session 內 CLI 與 MCP 並存呼叫同一服務**：狀態不一致的隱性 bug 來源。統一一條路。
  * **把 `Bash` tool 當萬靈丹，連簡單的 grep 都包成 MCP server**：shell 早就會做的事，硬包成 MCP 等於把簡單的事複雜化。CLI-first 的相反極端也要避免。
  * **fallback 切換沒在 `CLAUDE.md` 標明**：下個 session 又是新猜測。寫進規則檔。
  * **把 `gh pr list` 與 `mcp__github__list_pull_requests` 視為完全等價**：它們的 token 成本、可審計性、除錯路徑都不同。等價是錯覺。
</Warning>

## 自我檢核

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

  1. 面對一個新的整合需求，你能在 30 秒內判斷「CLI 可用還是需要 MCP」嗎？說出判斷依據。
  2. 你能否列出至少三條 CLI 相對 MCP 的具體優勢？每條配一個真實例子。
  3. 你目前 session（或最近一個 session）有沒有 CLI 與 MCP 混用同一服務的情況？列出、決定要不要統一。
  4. 你的 `CLAUDE.md` 有沒有寫明 CLI-first 偏好？若沒有，現在補一段。
</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；MCP transport、scope、tool search）

  * \[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；Bash tool 為核心工具、唯讀指令清單、allow / deny / ask 規則的 `Bash(specifier)` 形式、shell 識別子指令）

  * \[3] OpenAI, "Codex CLI," developers.openai.com, 2026. \[Online]. Available: [https://developers.openai.com/codex](https://developers.openai.com/codex) （截至 2026-06；codex CLI 基本指令、--approval-mode、沙箱執行、MCP config.toml 設定位置）

  * \[4] Google, "Gemini CLI," google-gemini.github.io, 2026. \[Online]. Available: [https://google-gemini.github.io/gemini-cli/](https://google-gemini.github.io/gemini-cli/) （截至 2026-06；gemini CLI 指令、shell 工具、--sandbox、MCP settings.json 設定、GEMINI\_API\_KEY 認證）

  * \[5] GitHub Docs, "GitHub Copilot CLI," docs.github.com, 2026. \[Online]. Available: [https://docs.github.com/en/copilot/how-tos/copilot-cli](https://docs.github.com/en/copilot/how-tos/copilot-cli) （截至 2026-06；gh copilot suggest / explain、gh auth login、\~/.copilot/mcp-config.json）

  * \[6] 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>

* MCP 整合設定見 [04-9 MCP 整合](/code-agent/customization/mcp-integration)。
* 內建工具盤點見 [04-8 各家 AI 的內建工具與功能](/code-agent/customization/vendor-builtin-tools)。
* `CLAUDE.md` 撰寫見 [04-1 CLAUDE.md 與記憶檔](/code-agent/customization/claude-md-memory)。
* Shell 工具的供應鏈風險見 [03-3 安全、隱私與供應鏈風險](/code-agent/judgment/security-privacy-supply-chain)。
* Bash 的可審計性與權限收斂見 [04-6 Hooks](/code-agent/customization/hooks)。
