Model Context Protocol (MCP) 是 Anthropic 推出的開放標準,讓 AI 助手能夠無縫連接外部工具與資料。但是,要怎麼從零開始實作一個 MCP Server呢?

在這篇文章中,將通過建立一個履歷評分器的實際案例,完整展示 MCP 的實作過程。我們會涵蓋從項目初始化、核心架構設計、到與 Claude Desktop 整合的每一個步驟。

什麼是 MCP?為什麼要學會實作它?

在開始之前,讓我們先理解 MCP 解決的核心問題:

傳統痛點: 當你在使用 Claude 或是像 ChatGPT 這類 LLM 工具時,多數情境下,LLM 能理解並評論一份履歷,但真正的痛點不在於做不做得到,而在於做不到標準化、可驗證、可重現:

  • 流程會斷:得在對話、表單、內部工具間來回切換,狀態與上下文常遺失。

  • 結果難重現:每次都靠人工貼資料+臨場指示,標準不一致。

  • 輸出不好用:模型多回自然語言,不是可驗證的 JSON,難進報表或自動化。

另外,確實也有 LLM 做不到或不該直接做的事,例如:

  • 直接連進你的資料庫(權限、稽核與安全控管)。

  • 真正操作瀏覽器做登入、點擊、擷取資料等自動化。

MCP 的解決方案: 讓 AI 助手直接調用你開發的工具,在同一個對話流程中完成所有操作。

這就像是為 AI 助手開發「套件」,讓它能夠執行自訂功能。

而我選「履歷評分」當範例,是因為具有需要可驗證的標準(量表、權重、結構化輸出),可以展現出 MCP 的價值。

直接先看實際運作畫面:

Demo

從 Demo 畫面中可以看到,可以調用「履歷評分器」工具,並且得到結構化的 JSON 輸出,包含各項評分指標和改進建議。接下來就來開始實作吧!

第一步:項目初始化與依賴管理

讓我們從建立一個新的 MCP 項目開始:

# 建立專案目錄
mkdir mcp-resume-scorer
cd mcp-resume-scorer

# 初始化 npm 專案
npm init -y

# 安裝 MCP SDK 和其他依賴
npm install @modelcontextprotocol/sdk openai ajv ajv-formats
npm install -D @types/node tsx typescript

專案結構規劃

mcp-resume-scorer/
├── src/
│   ├── server.ts            # MCP Server主程式
│   ├── utils/score.ts       # 評分邏輯
│   ├── llm/chatgpt.ts      # LLM 整合
│   ├── tools/ajv.ts        # 驗證工具
│   ├── schema/             # JSON Schema
│   └── prompts/            # 提示詞
├── server.js               # JavaScript 版本(生產用)
├── package.json
└── .gitignore              # 排除敏感文件

我們同時提供 TypeScript 和 JavaScript 兩個版本,TypeScript 用於開發,JavaScript 用於生產部署。

重要設計原則:環境變數管理

在 MCP 架構中,有一個關鍵的設計原則:所有敏感資訊(如 API 金鑰)都應該由 Claude Desktop 管理,而不是存放在專案中

為什麼這樣設計?

  1. 安全性:避免 API 金鑰意外提交到版本控制系統
  2. 統一管理:所有 MCP Server 的金鑰都在 Claude Desktop 中集中管理
  3. 環境隔離:開發、測試、生產環境可以使用不同的金鑰
  4. 符合最佳實踐:遵循「配置與程式碼分離」的原則

Claude Desktop 配置

{
  "mcpServers": {
    "resume-scorer": {
      "env": {
        "OPENAI_API_KEY": "sk-..."  // 在這裡設定
      }
    }
  }
}

啟動 Claude Desktop 時會自動載入環境變數

這樣設計的好處是:

  • 開發時:可以用 OPENAI_API_KEY=test npm run dev 測試
  • 生產時:完全由 Claude Desktop 管理,無需專案中存放金鑰

第二步:建立 MCP Server 核心

MCP Server 的核心是實作兩個關鍵功能:工具發現工具調用

基礎Server結構 (src/server.ts)

import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types";

async function main() {
  // 1. 建立 MCP Server實例
  const server = new Server(
    {
      name: "resume-scorer-mcp",      // Server名稱
      version: "0.1.0"                // 版本號
    },
    {
      capabilities: {
        tools: {}                      // 聲明支援工具功能
      }
    }
  );

  console.error("MCP Resume Scorer starting...");

  // 2. 註冊工具列表處理器
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
      tools: [
        {
          name: "resume_scorer",
          description: "【履歷評分器】專業AI履歷分析工具",
          inputSchema: {
            type: "object",
            required: ["resume_text"],
            properties: {
              resume_text: {
                type: "string",
                description: "履歷內容文字"
              }
            }
          }
        }
      ]
    };
  });

  // 3. 註冊工具調用處理器
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    if (request.params.name === "resume_scorer") {
      // 這裡會調用實際的評分邏輯
      const result = await scoreResume(request.params.arguments || {});
      return {
        content: [{ type: 'text', text: JSON.stringify(result) }]
      };
    }
    throw new Error(`Unknown tool: ${request.params.name}`);
  });

  // 4. 建立 Stdio 傳輸層並連接
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Resume Scorer connected and ready!");
}

main().catch(console.error);

關鍵概念解析

1. Server 建構函數

  • nameversion:用於識別你的 MCP Server
  • capabilities.tools:告訴客戶端這個Server支援工具功能

2. ListToolsRequestSchema 處理器

  • 當 Claude Desktop 連接時,會發送此請求來發現可用工具
  • 必須回傳工具列表,包含名稱、描述和輸入 Schema

3. CallToolRequestSchema 處理器

  • 當使用者觸發工具時,會發送此請求
  • request.params.name 是工具名稱
  • request.params.arguments 是輸入參數

4. StdioServerTransport

  • MCP 使用標準輸入/輸出進行通訊
  • 這讓 Claude Desktop 能夠啟動並與Server通訊

第三步:實作 OpenAI API 整合

接下來我們建立與 OpenAI 的連接模組:

LLM 整合 (src/llm/chatgpt.ts)

import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });

export async function chatJson({
  system,
  user,
  model = "gpt-4o-mini"
}: {
  system: string;
  user: string;
  model?: string
}) {
  const res = await client.chat.completions.create({
    model,
    messages: [
      { role: "system", content: system },
      { role: "user", content: user }
    ],
    response_format: { type: "json_object" },  // 強制 JSON 輸出
    temperature: 0.2                           // 降低隨機性
  });

  const msg = res.choices[0].message?.content ?? "{}";
  return {
    json: JSON.parse(msg),
    usage: res.usage,
    model: res.model
  };
}

重要設計決策

1. 強制 JSON 輸出

response_format: { type: "json_object" }

這確保 AI 回應必定是有效的 JSON,避免解析錯誤。

2. 低 Temperature 設定

temperature: 0.2

對於評分任務,我們需要一致性高於創意性。

3. 錯誤處理

const msg = res.choices[0].message?.content ?? "{}";

確保即使 API 回應異常,也有預設值可用。

第四步:實作業務邏輯 - 履歷評分器

現在我們來實作這個 MCP Server的核心功能:履歷評分。

JSON Schema 定義 (src/schema/resume_score.schema.json)

首先定義評分結果的資料結構:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": [
    "overall_score",
    "skill_completeness",
    "experience_quality",
    "achievement_showcase",
    "career_progression",
    "resume_structure",
    "summary",
    "recommendations"
  ],
  "properties": {
    "overall_score": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "description": "綜合評分 (0-100)"
    },
    "skill_completeness": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "description": "技能完整性評分"
    },
    "experience_quality": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "description": "經驗描述品質評分"
    },
    "achievement_showcase": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "description": "成果展示評分"
    },
    "career_progression": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "description": "職涯發展評分"
    },
    "resume_structure": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "description": "履歷結構評分"
    },
    "summary": {
      "type": "string",
      "description": "評分總結"
    },
    "recommendations": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "改進建議"
    }
  }
}

AI 提示詞設計 (src/prompts/system.txt)

建立專業的評分提示詞:

你是一個專業的履歷分析專家。你的任務是對履歷進行全方位的結構化分析。

請根據以下評分維度來評估履歷品質:

1. **技能完整性 (skill_completeness)**: 技能列表是否完整、相關且有說服力
2. **經驗描述品質 (experience_quality)**: 工作經驗描述是否詳細、具體且有價值
3. **成果展示 (achievement_showcase)**: 是否有具體數據、成就和影響力展示
4. **職涯發展 (career_progression)**: 職涯軌跡是否合理、有成長性
5. **履歷結構 (resume_structure)**: 格式、組織、表達是否清晰專業

評分標準:
- 0-30分:需要大幅改進
- 31-50分:有明顯不足
- 51-70分:基本合格
- 71-85分:品質良好
- 86-100分:優秀出色

如果同時提供職缺描述,則額外分析匹配度。

請提供具體的評分理由和針對性的改進建議。

輸出必須是有效的 JSON 格式,嚴格遵循提供的 Schema。

JSON Schema 驗證工具 (src/tools/ajv.ts)

建立驗證工具:

import Ajv from "ajv";
import addFormats from "ajv-formats";

export function makeAjv() {
  const ajv = new Ajv({
    allErrors: true,    // 顯示所有錯誤
    strict: false       // 允許額外屬性
  });
  addFormats(ajv);      // 添加格式驗證(日期、email 等)
  return ajv;
}

核心評分邏輯 (src/utils/score.ts)

現在實作完整的評分工具:

import { Tool } from "@modelcontextprotocol/sdk/types";
import { chatJson } from "../llm/chatgpt";
import { makeAjv } from "../tools/ajv";
import schema from "../schema/resume_score.schema.json";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

// 取得當前文件路徑(ES modules 需要)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 定義權重類型
type Weights = {
  skill_completeness: number;
  experience_quality: number;
  achievement_showcase: number;
  career_progression: number;
  resume_structure: number;
};

export const scoreResumeTool: Tool<{
  resume_text: string;
  job_text?: string;
  weights?: Weights;
  model?: string;
}> = {
  name: "resume_scorer",
  description: "【履歷評分器】專業 AI 履歷分析工具,提供 5 大維度評分和改進建議",

  // 定義輸入參數 Schema
  inputSchema: {
    type: "object",
    required: ["resume_text"],
    properties: {
      resume_text: {
        type: "string",
        description: "履歷內容文字"
      },
      job_text: {
        type: "string",
        description: "職缺描述(選填,用於匹配度分析)"
      },
      weights: {
        type: "object",
        description: "評分權重設定(選填)"
      },
      model: {
        type: "string",
        description: "使用的 AI 模型(選填)"
      }
    }
  },

  // 實作評分邏輯
  async *invoke({ resume_text, job_text, weights, model }) {
    try {
      // 1. 設定預設權重
      const defaultWeights: Weights = {
        skill_completeness: 0.25,      // 技能完整性 25%
        experience_quality: 0.25,      // 經驗品質 25%
        achievement_showcase: 0.20,    // 成果展示 20%
        career_progression: 0.15,      // 職涯發展 15%
        resume_structure: 0.15         // 履歷結構 15%
      };
      const w = { ...defaultWeights, ...weights };

      // 2. 讀取系統提示詞
      const systemPromptPath = path.join(__dirname, "../prompts/system.txt");
      const system = fs.readFileSync(systemPromptPath, "utf8");

      // 3. 組合使用者輸入
      const user = job_text ? [
        "【履歷內容】", resume_text,
        "【職缺描述】", job_text,
        "【評分權重】", JSON.stringify(w),
        "【Schema】", JSON.stringify(schema, null, 2),
        "【產出要求】只輸出 JSON,符合 Schema。進行履歷分析並額外評估與職缺的匹配度。"
      ].join("\n\n") : [
        "【履歷內容】", resume_text,
        "【評分權重】", JSON.stringify(w),
        "【Schema】", JSON.stringify(schema, null, 2),
        "【產出要求】只輸出 JSON,符合 Schema。專注於履歷本身的品質分析。"
      ].join("\n\n");

      // 4. 調用 AI 進行評分
      const { json, usage, model: usedModel } = await chatJson({
        system,
        user,
        model
      });

      // 5. 驗證輸出格式
      const ajv = makeAjv();
      const validate = ajv.compile(schema as any);
      const ok = validate(json);

      if (!ok) {
        return {
          error: "Schema 驗證失敗",
          ajvErrors: validate.errors,
          raw: json,
          success: false
        };
      }

      // 6. 回傳成功結果
      return {
        ...json,
        tokens: usage,
        model: usedModel,
        version: "0.1.0",
        success: true
      };

    } catch (error) {
      console.error("Resume scorer error:", error);
      return {
        error: error instanceof Error ? error.message : "未知錯誤",
        success: false,
        timestamp: new Date().toISOString()
      };
    }
  }
};

關鍵實作細節解析

1. Generator 函數使用

async *invoke({ ... }) {

MCP Tool 使用 Generator 函數,這允許未來支援串流輸出。

2. 權重系統設計

const defaultWeights: Weights = {
  skill_completeness: 0.25,    // 技能占比最高
  experience_quality: 0.25,    // 經驗同等重要
  achievement_showcase: 0.20,  // 成果展示次之
  career_progression: 0.15,    // 職涯發展
  resume_structure: 0.15       // 結構清晰度
};

這個權重分配反映了履歷評估的優先順序。

3. 條件性提示詞組合

const user = job_text ? [
  // 有職缺描述時的提示詞
] : [
  // 只有履歷時的提示詞
];

根據是否提供職缺描述,動態調整 AI 的分析重點。

4. 多層錯誤處理

  • API 調用錯誤
  • JSON 解析錯誤
  • Schema 驗證錯誤
  • 未知錯誤

第五步:建立生產環境版本

為了提高相容性和啟動速度,我們建立一個 JavaScript 版本用於生產環境。

生產環境Server (server.js)

這是一個完整的 JavaScript 版本,將所有模組整合在一個文件中:

const { Server } = require("@modelcontextprotocol/sdk/dist/cjs/server/index");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/dist/cjs/server/stdio");
const fs = require("fs");
const path = require("path");
const OpenAI = require("openai");

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// 內嵌 JSON Schema(避免文件路徑問題)
const schema = {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["overall_score", "skill_completeness", "experience_quality",
               "achievement_showcase", "career_progression", "resume_structure",
               "summary", "recommendations"],
  "properties": {
    "overall_score": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "description": "綜合評分 (0-100)"
    },
    // ... 其他屬性
  }
};

// OpenAI API 整合
async function chatJson({ system, user, model = "gpt-4o-mini" }) {
  const res = await client.chat.completions.create({
    model,
    messages: [
      { role: "system", content: system },
      { role: "user", content: user }
    ],
    response_format: { type: "json_object" },
    temperature: 0.2
  });
  const msg = (res.choices[0].message && res.choices[0].message.content) || "{}";
  return { json: JSON.parse(msg), usage: res.usage, model: res.model };
}

// 履歷評分函數
async function scoreResume({ resume_text, job_text, weights, model }) {
  try {
    const defaultWeights = {
      skill_completeness: 0.25,
      experience_quality: 0.25,
      achievement_showcase: 0.20,
      career_progression: 0.15,
      resume_structure: 0.15
    };
    const w = Object.assign({}, defaultWeights, weights || {});

    // 讀取系統提示詞
    const systemPromptPath = path.join(__dirname, "src", "prompts", "system.txt");
    const system = fs.readFileSync(systemPromptPath, "utf8");

    // 組合提示詞
    const user = job_text ? [
      "【履歷內容】", resume_text,
      "【職缺描述】", job_text,
      "【評分權重】", JSON.stringify(w),
      "【Schema】", JSON.stringify(schema, null, 2),
      "【產出要求】只輸出 JSON,符合 Schema。進行履歷分析並額外評估與職缺的匹配度。"
    ].join("\n\n") : [
      "【履歷內容】", resume_text,
      "【評分權重】", JSON.stringify(w),
      "【Schema】", JSON.stringify(schema, null, 2),
      "【產出要求】只輸出 JSON,符合 Schema。專注於履歷本身的品質分析。"
    ].join("\n\n");

    const { json, usage, model: usedModel } = await chatJson({ system, user, model });

    return Object.assign({}, json, {
      tokens: usage,
      model: usedModel,
      version: "0.1.0",
      success: true
    });
  } catch (error) {
    console.error("Resume scorer error:", error);
    return {
      error: error instanceof Error ? error.message : "未知錯誤",
      success: false,
      timestamp: new Date().toISOString()
    };
  }
}

// 主程式
async function main() {
  const server = new Server({
    name: "resume-scorer-mcp",
    version: "0.1.0"
  }, {
    capabilities: { tools: {} }
  });

  console.error("MCP Resume Scorer starting...");

  const { ListToolsRequestSchema, CallToolRequestSchema } =
    require("@modelcontextprotocol/sdk/dist/cjs/types");

  // 工具發現
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: [{
      name: "resume_scorer",
      description: "【履歷評分器】專業AI履歷分析工具,提供5大維度評分和改進建議",
      inputSchema: {
        type: "object",
        required: ["resume_text"],
        properties: {
          resume_text: { type: "string", description: "履歷內容文字" },
          job_text: { type: "string", description: "職缺描述(選填)" },
          weights: { type: "object", description: "評分權重設定(選填)" },
          model: { type: "string", description: "使用的AI模型(選填)" }
        }
      }
    }]
  }));

  // 工具調用
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    if (request.params.name === "resume_scorer") {
      const result = await scoreResume(request.params.arguments || {});
      return {
        content: [{ type: 'text', text: JSON.stringify(result) }]
      };
    }
    throw new Error(`Unknown tool: ${request.params.name}`);
  });

  // 連接傳輸層
  const transport = new StdioServerTransport();
  console.error("Connecting to transport...");
  await server.connect(transport);
  console.error("MCP Resume Scorer connected and ready!");

  // 優雅關閉
  process.on('SIGINT', async () => {
    console.error("Shutting down server...");
    await server.close();
    process.exit(0);
  });
}

main().catch((error) => {
  console.error("Failed to start server:", error);
  process.exit(1);
});

package.json 腳本設定

{
  "name": "mcp-resume-scorer",
  "version": "0.1.0",
  "description": "MCP server for scoring resumes against job descriptions using AI",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "npx tsx watch src/server.ts",
    "start:ts": "npx tsx src/server.ts",
    "build": "echo 'Using JavaScript server.js - no build needed' && exit 0"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.20.2",
    "ajv": "^8.17.1",
    "ajv-formats": "^3.0.1",
    "openai": "^6.7.0"
  },
  "devDependencies": {
    "@types/node": "^24.10.0",
    "tsx": "^4.20.6",
    "typescript": "^5.9.3"
  }
}

第六步:與 Claude Desktop 整合

Claude Desktop 配置

macOS 配置位置:

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

Windows 配置位置:

%APPDATA%\Claude\claude_desktop_config.json

正確的配置內容:

{
  "mcpServers": {
    "resume-scorer": {
      "command": "node",
      "args": ["/absolute/path/to/your/mcp-resume-scorer/server.js"],
      "env": {
        "OPENAI_API_KEY": "your_openai_api_key_here"
      }
    }
  }
}

實際在 Claude Desktop 中應該長這樣:

Claude Desktop MCP Config

測試與驗證

1. 重啟 Claude Desktop 配置完成後,重啟 Claude Desktop 應用程式。

2. 觸發履歷評分器 在對話中使用關鍵字觸發工具:

請使用履歷評分器分析這份履歷:

我是一名有5年經驗的全端工程師,專精於 React、Node.js 和 PostgreSQL。
在上一份工作中,我主導開發了一個電商平台,成功提升轉換率 35%。
具有敏捷開發經驗,曾帶領 3 人團隊完成多個專案。

請提供詳細的技能完整性、經驗品質、成果展示、履歷結構、職涯發展評分。

3. 預期輸出格式

{
  "overall_score": 82,
  "skill_completeness": 85,
  "experience_quality": 88,
  "achievement_showcase": 80,
  "career_progression": 75,
  "resume_structure": 82,
  "summary": "這份履歷展現了優秀的全端開發能力...",
  "recommendations": [
    "建議增加具體的技術架構描述",
    "可以補充更多量化的業務成果"
  ],
  "tokens": { "prompt_tokens": 245, "completion_tokens": 156 },
  "model": "gpt-4o-mini",
  "version": "0.1.0",
  "success": true
}

嘗試用另一份履歷來測試,可以看到會輸出相同格式的評分結果。

Claude Desktop MCP Tool Usage

總結

通過這個完整的 MCP 履歷評分器專案,我們可以學到:

1. MCP 協議理解

2. 架構設計原則

3. AI 整合最佳實踐

而這個 MCP 履歷評分器不只是一個技術展示,它實際解決了:

  • 標準化評分:消除主觀偏見
  • 效率提升:自動化重複性工作
  • 一致性保證:每次評分都遵循相同標準

MCP 為 AI 工具的整合提供了標準化的解決方案,讓我們能夠建立更加智慧和高效的工作流程。這個履歷評分器只是開始——想像一下,當你所有專業工具都能夠在同一個 AI 對話中無縫協作,應該會是一個很棒的開發體驗。

完整專案可參考 Repo