自己的 MCP Server 自己包:從既有 Python 腳本到 Claude 可呼叫的工具,只要 15 分鐘
背景
上一篇我評估了兩個讓 AI 操控剪輯軟體的 MCP 工具,結論是:一個免費但用處有限,一個功能強但需要付費版 DaVinci Resolve。兩個都沒裝。
但那次評估讓我想到一件事:與其等別人做出完美的工具,不如把我自己已經在用的腳本包成 MCP server?
我的 Code Fossil YouTube 頻道有一支 timeline-to-fcpxml.py,347 行純 Python,讀 timeline.json 產出 DaVinci Resolve 可匯入的 FCPXML。每次要用都得開終端機下指令。如果 Claude 能直接呼叫它,不就省了這個步驟?
發現過程
工具選擇:uv + FastMCP
MCP server 的 Python SDK 叫 FastMCP,裝法最簡單的是搭配 uv(Rust 寫的 Python 套件管理工具)。Claude Code 的 MCP 文件也推薦這個組合。
brew install uv
關鍵設計決策:importlib 載入,不複製程式碼
最大的問題是:MCP server 怎麼用既有腳本的邏輯?
選項一:複製核心函式到 server.py — 簡單但會 drift,改了一邊忘了另一邊。
選項二:把既有腳本改成 package — 動太大,而且 CLI 用法會壞掉。
選項三:用 importlib 直接載入既有的 .py 檔 — 零修改,單一來源。
我選了選項三:
import importlib.util
spec = importlib.util.spec_from_file_location(
"timeline_to_fcpxml",
os.path.join(TOOLS_DIR, "timeline-to-fcpxml.py"),
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
# 直接呼叫 mod.load_timeline(), mod.build_fcpxml()
既有腳本底部有 if __name__ == "__main__": main(),所以載入時不會觸發 main()。原本的 CLI 用法完全不受影響。
三個工具就夠
參考現成的 fcpxml-mcp-server(53 個工具),我只做了三個:
generate_fcpxml— 讀 timeline.json,產出 FCPXML 檔案validate_timeline— 檢查 timeline 的統計和潛在問題(gap、缺素材、zero-duration)preview_fcpxml— 在記憶體產出 FCPXML 回傳前 N 行,不寫檔
為什麼不做更多?因為 Claude Code 本身已經有檔案系統存取能力(Read、Glob、Grep),做一個 list_assets 工具只是重複它已經會的事。
踩到的坑:路徑解析
uv --directory tools/mcp-server run server.py 會把工作目錄設到 tools/mcp-server/,但 timeline.json 在 output/ 底下。Claude 傳進來的路徑可能是相對路徑(output/timeline.json)也可能是絕對路徑。
解法:所有路徑參數都透過一個 _resolve_path() 函式,相對路徑自動接上 project root:
_PROJECT_ROOT = os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__))
))
def _resolve_path(path: str) -> str:
if os.path.isabs(path):
return path
return os.path.join(_PROJECT_ROOT, path)
3 分鐘快速上手
裝好 uv 之後,要在自己的專案裡包一個 MCP server:
1. 建立專案結構
mkdir -p tools/mcp-server
cd tools/mcp-server
建立 pyproject.toml:
[project]
name = "my-mcp-server"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["mcp[cli]>=1.2.0"]
2. 寫 server.py(最小範例)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-tool")
@mcp.tool()
def hello(name: str) -> str:
"""Say hello."""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run(transport="stdio")
3. 測試
# 送一個 initialize 請求,看有沒有回應
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \
| uv --directory tools/mcp-server run python server.py
看到 JSON response 就成功了。接著在專案根目錄建 .mcp.json 註冊:
{
"mcpServers": {
"my-tool": {
"type": "stdio",
"command": "uv",
"args": ["--directory", "/absolute/path/to/tools/mcp-server", "run", "python", "server.py"]
}
}
}
下次開 Claude Code 就會自動載入。
具體數據 / 結果
| 指標 | 數值 |
|---|---|
| 新增檔案 | 2(server.py + pyproject.toml) |
| 新增程式碼 | ~150 行 |
| 修改既有程式碼 | 0 行 |
| 暴露的 MCP 工具 | 3 個 |
| 從動手到跑通 | ~15 分鐘 |
| 依賴安裝 | uv(brew install)+ mcp[cli](uv 自動管理) |
實際呼叫 validate_timeline 的結果:
Timeline: output/timeline.json
Entries: 59
Duration: 453s (7.5 min)
Types: {"vhs": 4, "text-card": 30, "screenshot": 18, "gap": 7}
With assets: 52/59
Issues found:
Missing assets (7): b05-black, b09-face, b14-face...
Timeline gaps:
Gap between hook-06 and b01-01: 1.00s
Gap between b05-black and b06-01: 11.90s
以前要開終端機跑指令才看得到這些,現在 Claude 可以直接呼叫、直接分析。
反思
技術面
MCP server 的本質就是 JSON-RPC over stdio。FastMCP 把所有協議細節藏起來,你只要寫 @mcp.tool() 裝飾器就好。實際的認知負擔比我想像的低很多——跟寫一個 Flask route 差不多。
importlib 載入既有腳本是個很實用的技巧。不用重構既有程式碼,不用改檔名(Python 不喜歡檔名有 hyphen,但 importlib 不在乎),原本的 CLI 和新的 MCP server 共用同一份邏輯。
心態面
上一篇的結論是「不裝」,這一篇的結論是「自己做比想像中簡單」。兩個結論不矛盾——重點是你得先搞清楚自己的需求,再決定是用現成的、自己做、還是都不做。
我一開始被那些 53 工具、21 工具的規模嚇到,以為寫 MCP server 很複雜。實際做了才發現,對自己的用途,3 個工具、150 行就夠了。
有趣發現
寫 MCP server 最有價值的部分不是 generate_fcpxml(那個 CLI 也能做),而是 validate_timeline。因為它讓 Claude 可以主動檢查 timeline 的問題,而不是等我人工發現。這種「讓 AI 有能力自己做 QC」的模式,比我預期的更有用。