返回AI笔记
AI Day 56

AI Day 56: 实战(6):MCP Server开发 — 扩展AI的能力边界

AI Day 56: 实战(6):MCP Server开发 — 扩展AI的能力边界

2026-05-27

日期: 2026-05-27 | 阶段: 第五阶段 · 动手实战 (Day 51-60) | 主题: MCP Server Development

学习路径 / Learning Path

AI/LLM 深度技术学习 60天计划
├── 第一阶段:模型基础 (Day 1-15) ✅
│   ├── Day 1: Transformer与LLM基础 ✅
│   ├── Day 2: 量化与本地部署 ✅
│   ├── Day 3: 训练全流程 ✅
│   ├── Day 4: Prompt Engineering ✅
│   ├── Day 5: RAG架构 ✅
│   ├── Day 6: 向量数据库与Embedding ✅
│   ├── Day 7: 微调技术 ✅
│   ├── Day 8: 推理优化 ✅
│   ├── Day 9: 长上下文技术 ✅
│   ├── Day 10: 多模态模型 ✅
│   ├── Day 11: 推理模型 ✅
│   ├── Day 12: Agent框架 ✅
│   ├── Day 13: MCP协议 ✅
│   ├── Day 14: 模型评估 ✅
│   └── Day 15: 阶段一总结 ✅
├── 第二阶段:工程实践 (Day 16-30) ✅
│   ├── Day 16: LLM应用架构 ✅
│   ├── Day 17: 安全与护栏 ✅
│   ├── Day 18: 可观测性 ✅
│   ├── Day 19: 生产RAG·解析与分块 ✅
│   ├── Day 20: 生产RAG·检索与重排 ✅
│   ├── Day 21: 生产RAG·评估与迭代 ✅
│   ├── Day 22: Agent状态与恢复 ✅
│   ├── Day 23: Agent成本优化 ✅
│   ├── Day 24: 多Agent系统 ✅
│   ├── Day 25: Agent测试部署 ✅
│   ├── Day 26: LLM成本工程 ✅
│   ├── Day 27: 多模型编排 ✅
│   ├── Day 28: LLM应用测试 ✅
│   ├── Day 29: 企业LLM平台 ✅
│   └── Day 30: 阶段二总结 ✅
├── 第三阶段:金融零售AI应用 (Day 31-42) ✅
│   ├── Day 31: 金融AI风控 ✅
│   ├── Day 32: 智能投顾与量化 ✅
│   ├── Day 33: 合规与RegTech ✅
│   ├── Day 34: 信贷AI全链路 ✅
│   ├── Day 35: 金融AI总结 ✅
│   ├── Day 36: 零售AI推荐 ✅
│   ├── Day 37: 智能客服 ✅
│   ├── Day 38: 供应链AI ✅
│   ├── Day 39: 智能营销 ✅
│   ├── Day 40: 零售AI总结 ✅
│   ├── Day 41: CeFi×DeFi×AI融合 ✅
│   └── Day 42: AI融合案例与职业 ✅
├── 第四阶段:面试冲刺 (Day 43-50) ✅
│   ├── Day 43: 系统设计·LLM平台 ✅
│   ├── Day 44: 系统设计·RAG系统 ✅
│   ├── Day 45: 系统设计·Agent系统 ✅
│   ├── Day 46: 系统设计·推荐系统 ✅
│   ├── Day 47: 面试·产品AI ✅
│   ├── Day 48: 面试·架构AI ✅
│   ├── Day 49: 面试·行为AI ✅
│   └── Day 50: 学习总结 ✅
└── 第五阶段:动手实战 (Day 51-60)
    ├── Day 51: 本地大模型部署全流程 ✅
    ├── Day 52: RAG系统实战:从文档到问答 ✅
    ├── Day 53: RAG进阶:评估优化与生产化 ✅
    ├── Day 54: LoRA微调实战:训练你的专属模型 ✅
    ├── Day 55: Agent开发实战:构建工具调用Agent ✅
    ├── Day 56: MCP Server开发:扩展AI能力边界 ← 你在这里
    ├── Day 57: 多模态应用:图文理解与文档分析
    ├── Day 58: AI应用全栈开发:前后端集成
    ├── Day 59: 性能调优与成本实战
    └── Day 60: 总结与作品集

核心概念 / Core Concepts

MCP 让你的工具能被任何 AI 客户端调用 / One Server, Every Client

Day 55 的 Agent 工具 vs Day 56 的 MCP Server:

Day 55 Agent 工具:
  get_token_price() ──→ 只有你的 Agent 能用
  get_protocol_tvl() ──→ 写在代码里,绑定框架
  web_search()       ──→ 换个项目要重写

Day 56 MCP Server:
  get_token_price() ──→ Claude Code 能用
                    ──→ Cursor 能用
                    ──→ Windsurf 能用
                    ──→ 任何 MCP 客户端都能用
                    ──→ 一次开发,处处可用!

这就是 MCP 的核心价值:标准化的工具接口协议

Day 13 理论回顾 → 今天实践 / Theory Recap → Practice

Day 13 学了 MCP 的架构:

┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │────→│ Protocol │────→│  Server  │
│ (Claude) │←────│  (MCP)   │←────│ (你开发) │
└──────────┘     └──────────┘     └──────────┘

MCP Server 提供三种能力:
1. Tools   — 可调用的函数(类似 API)
2. Resources — 可读取的数据(类似文件系统)
3. Prompts  — 预定义的提示模板

今天的目标:
  理论                       实践
  Day 13 "MCP 有三种能力"  → 实际实现 Tools + Resources
  Day 13 "JSON-RPC 通信"   → 用 SDK 处理底层协议
  Day 13 "stdio 传输"      → 让 Claude Code 连接你的 Server

从"知道 MCP 是什么"到"发布一个真正的 MCP Server"!

知识点1:MCP SDK 安装与项目初始化 / MCP SDK Setup

TypeScript SDK 选择 / Why TypeScript

MCP 有两个官方 SDK:
  TypeScript SDK — 更成熟、示例更多、npm 生态
  Python SDK    — 也可以,但社区资源较少

选择 TypeScript 的理由:
1. MCP 诞生于 Anthropic,生态以 TS 为主
2. npm 发布更方便(MCP Server 通常是 CLI 工具)
3. 类型系统帮助保证协议正确性
4. 大多数开源 MCP Server 都是 TS 写的

不用担心不会 TS:
  今天的代码不复杂
  主要是填充 MCP SDK 的模板
  核心逻辑和 Day 55 的 Python 工具类似

项目初始化 / Project Initialization

# === Step 1: 创建项目目录 ===
mkdir mcp-notes-server
cd mcp-notes-server

# === Step 2: 初始化 npm 项目 ===
npm init -y

# === Step 3: 安装 MCP SDK ===
npm install @modelcontextprotocol/sdk

# === Step 4: 安装 TypeScript 相关 ===
npm install -D typescript @types/node tsx

# === Step 5: 初始化 TypeScript ===
npx tsc --init

# === Step 6: 其他依赖 ===
npm install zod           # 输入验证
npm install glob          # 文件匹配
npm install gray-matter   # Markdown frontmatter 解析

项目结构 / Project Structure

mcp-notes-server/
├── src/
│   ├── index.ts          # MCP Server 入口
│   ├── tools/
│   │   ├── search.ts     # 笔记搜索工具
│   │   └── content.ts    # 内容获取工具
│   ├── resources/
│   │   └── notes.ts      # 笔记资源定义
│   └── utils/
│       ├── markdown.ts   # Markdown 解析
│       └── search.ts     # 搜索算法
├── package.json
├── tsconfig.json
└── README.md

tsconfig.json 配置 / TypeScript Config

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

package.json 关键配置 / Package.json

{
  "name": "mcp-notes-server",
  "version": "1.0.0",
  "description": "MCP Server for searching and reading learning notes",
  "type": "module",
  "bin": {
    "mcp-notes-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js"
  },
  "keywords": ["mcp", "notes", "search", "ai"],
  "license": "MIT"
}

完整实现 / Full Implementation

/**
 * src/index.ts
 * MCP Notes Server — 让 AI 能搜索和读取你的学习笔记
 *
 * 提供的能力:
 * - Tool: search_notes  — 搜索笔记内容
 * - Tool: get_note_content — 获取完整笔记内容
 * - Resource: notes://list — 浏览所有笔记列表
 *
 * Day 13 学的 MCP 协议 → 今天的实际实现
 */

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs";
import * as path from "path";
import { glob } from "glob";

// ==========================================
// 配置
// ==========================================

const NOTES_DIR = process.env.NOTES_DIR || "E:/code/momofinance/momoweb3/docs/ai";

// ==========================================
// 工具函数
// ==========================================

interface NoteInfo {
  filename: string;
  title: string;
  day: number;
  path: string;
  size: number;
}

/**
 * 扫描笔记目录,获取所有笔记信息
 */
function scanNotes(): NoteInfo[] {
  const pattern = path.join(NOTES_DIR, "day*.md").replace(/\\/g, "/");
  const files = glob.sync(pattern);

  return files.map((filePath) => {
    const filename = path.basename(filePath);
    const content = fs.readFileSync(filePath, "utf-8");
    const firstLine = content.split("\n")[0] || "";
    const title = firstLine.replace(/^#\s*/, "").trim();
    const dayMatch = filename.match(/day(\d+)/);
    const day = dayMatch ? parseInt(dayMatch[1]) : 0;

    return {
      filename,
      title,
      day,
      path: filePath,
      size: content.length,
    };
  }).sort((a, b) => a.day - b.day);
}

/**
 * 在笔记中搜索关键词
 */
function searchInNotes(
  query: string,
  maxResults: number = 5
): Array<{ filename: string; title: string; matches: string[]; score: number }> {
  const notes = scanNotes();
  const queryLower = query.toLowerCase();
  const queryTerms = queryLower.split(/\s+/).filter(Boolean);

  const results: Array<{
    filename: string;
    title: string;
    matches: string[];
    score: number;
  }> = [];

  for (const note of notes) {
    const content = fs.readFileSync(note.path, "utf-8");
    const contentLower = content.toLowerCase();

    // 计算相关性分数
    let score = 0;
    const matches: string[] = [];

    for (const term of queryTerms) {
      // 标题匹配权重更高
      if (note.title.toLowerCase().includes(term)) {
        score += 10;
      }

      // 内容匹配
      const termCount = (contentLower.match(new RegExp(term, "g")) || []).length;
      score += termCount;

      // 提取匹配上下文
      if (termCount > 0) {
        const idx = contentLower.indexOf(term);
        const start = Math.max(0, idx - 50);
        const end = Math.min(content.length, idx + term.length + 100);
        const snippet = content.substring(start, end).replace(/\n/g, " ").trim();
        if (snippet && !matches.includes(snippet)) {
          matches.push(`...${snippet}...`);
        }
      }
    }

    if (score > 0) {
      results.push({
        filename: note.filename,
        title: note.title,
        matches: matches.slice(0, 3), // 最多3个匹配片段
        score,
      });
    }
  }

  // 按分数排序
  return results
    .sort((a, b) => b.score - a.score)
    .slice(0, maxResults);
}

// ==========================================
// 创建 MCP Server
// ==========================================

const server = new Server(
  {
    name: "mcp-notes-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);

// ==========================================
// 注册 Tools
// ==========================================

/**
 * 列出所有可用工具
 */
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "search_notes",
        description:
          "搜索学习笔记。输入关键词,返回最相关的笔记和匹配内容片段。" +
          "支持中英文搜索。" +
          "适用于查找特定知识点、概念定义、或相关笔记。",
        inputSchema: {
          type: "object" as const,
          properties: {
            query: {
              type: "string",
              description: "搜索关键词,如 'LoRA微调' 或 'RAG evaluation'",
            },
            max_results: {
              type: "number",
              description: "最大返回结果数,默认5",
              default: 5,
            },
          },
          required: ["query"],
        },
      },
      {
        name: "get_note_content",
        description:
          "获取指定笔记的完整内容。" +
          "输入笔记的文件名(如 'day7-finetuning-lora-qlora.md'),返回完整的 Markdown 内容。" +
          "通常先用 search_notes 找到相关笔记,再用此工具获取详细内容。",
        inputSchema: {
          type: "object" as const,
          properties: {
            filename: {
              type: "string",
              description: "笔记文件名,如 'day7-finetuning-lora-qlora.md'",
            },
            section: {
              type: "string",
              description: "可选,只返回包含该关键词的章节",
            },
          },
          required: ["filename"],
        },
      },
    ],
  };
});

/**
 * 执行工具调用
 */
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "search_notes": {
      const query = (args as { query: string; max_results?: number }).query;
      const maxResults = (args as { max_results?: number }).max_results || 5;

      const results = searchInNotes(query, maxResults);

      if (results.length === 0) {
        return {
          content: [
            {
              type: "text" as const,
              text: `未找到与"${query}"相关的笔记。请尝试其他关键词。`,
            },
          ],
        };
      }

      const formatted = results.map((r, i) =>
        `[${i + 1}] ${r.filename}\n` +
        `    标题: ${r.title}\n` +
        `    相关性: ${r.score}\n` +
        `    匹配内容:\n${r.matches.map(m => `      ${m}`).join("\n")}`
      ).join("\n\n");

      return {
        content: [
          {
            type: "text" as const,
            text: `找到 ${results.length} 个相关笔记:\n\n${formatted}`,
          },
        ],
      };
    }

    case "get_note_content": {
      const filename = (args as { filename: string; section?: string }).filename;
      const section = (args as { section?: string }).section;

      const filePath = path.join(NOTES_DIR, filename);

      if (!fs.existsSync(filePath)) {
        return {
          content: [
            {
              type: "text" as const,
              text: `文件不存在: ${filename}。请使用 search_notes 查找正确的文件名。`,
            },
          ],
          isError: true,
        };
      }

      let content = fs.readFileSync(filePath, "utf-8");

      // 如果指定了 section,只返回相关部分
      if (section) {
        const sectionLower = section.toLowerCase();
        const lines = content.split("\n");
        const relevantSections: string[] = [];
        let capturing = false;
        let currentSection: string[] = [];

        for (const line of lines) {
          if (line.match(/^#{1,3}\s/)) {
            // 新的标题行
            if (capturing && currentSection.length > 0) {
              relevantSections.push(currentSection.join("\n"));
            }
            capturing = line.toLowerCase().includes(sectionLower);
            currentSection = capturing ? [line] : [];
          } else if (capturing) {
            currentSection.push(line);
          }
        }

        if (capturing && currentSection.length > 0) {
          relevantSections.push(currentSection.join("\n"));
        }

        if (relevantSections.length > 0) {
          content = relevantSections.join("\n\n---\n\n");
        } else {
          content = `未找到包含"${section}"的章节。\n\n完整内容(前2000字):\n\n${content.slice(0, 2000)}...`;
        }
      }

      // 截断过长内容
      if (content.length > 5000) {
        content = content.slice(0, 5000) + `\n\n... (内容过长,已截断。共 ${content.length} 字)`;
      }

      return {
        content: [
          {
            type: "text" as const,
            text: content,
          },
        ],
      };
    }

    default:
      return {
        content: [
          {
            type: "text" as const,
            text: `未知工具: ${name}`,
          },
        ],
        isError: true,
      };
  }
});

// ==========================================
// 注册 Resources
// ==========================================

/**
 * 列出所有可用资源
 */
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  const notes = scanNotes();

  return {
    resources: [
      {
        uri: "notes://list",
        name: "学习笔记列表",
        description: `所有 AI/LLM 学习笔记索引 (共 ${notes.length} 篇)`,
        mimeType: "application/json",
      },
      // 每个笔记也是一个资源
      ...notes.map((note) => ({
        uri: `notes://${note.filename}`,
        name: `Day ${note.day}: ${note.title}`,
        description: `学习笔记 ${note.filename} (${Math.round(note.size / 1024)}KB)`,
        mimeType: "text/markdown",
      })),
    ],
  };
});

/**
 * 读取资源内容
 */
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const uri = request.params.uri;

  if (uri === "notes://list") {
    const notes = scanNotes();
    const list = notes.map((n) => ({
      day: n.day,
      filename: n.filename,
      title: n.title,
      size_kb: Math.round(n.size / 1024),
    }));

    return {
      contents: [
        {
          uri,
          mimeType: "application/json",
          text: JSON.stringify(list, null, 2),
        },
      ],
    };
  }

  // notes://day7-finetuning-lora-qlora.md
  const filename = uri.replace("notes://", "");
  const filePath = path.join(NOTES_DIR, filename);

  if (!fs.existsSync(filePath)) {
    throw new Error(`Resource not found: ${uri}`);
  }

  const content = fs.readFileSync(filePath, "utf-8");

  return {
    contents: [
      {
        uri,
        mimeType: "text/markdown",
        text: content,
      },
    ],
  };
});

// ==========================================
// 启动 Server
// ==========================================

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Notes Server running on stdio");
  console.error(`Notes directory: ${NOTES_DIR}`);
  console.error(`Notes found: ${scanNotes().length}`);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

知识点3:高级功能 / Advanced Features

Prompt 模板 / Prompt Templates

/**
 * src/prompts.ts
 * MCP Prompt 模板 — 预定义的提示词,客户端可以直接使用
 *
 * Day 4 学的 Prompt Engineering 技巧
 * → 封装成 MCP Prompt 让任何客户端使用
 */

import {
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";

export function registerPrompts(server: Server) {
  /**
   * 列出所有 Prompt 模板
   */
  server.setRequestHandler(ListPromptsRequestSchema, async () => {
    return {
      prompts: [
        {
          name: "summarize_day",
          description: "总结指定Day的学习笔记,提取关键知识点",
          arguments: [
            {
              name: "day_number",
              description: "Day编号,如 7",
              required: true,
            },
          ],
        },
        {
          name: "compare_concepts",
          description: "对比两个概念的异同(从笔记中提取信息)",
          arguments: [
            {
              name: "concept_a",
              description: "第一个概念,如 'RAG'",
              required: true,
            },
            {
              name: "concept_b",
              description: "第二个概念,如 '微调'",
              required: true,
            },
          ],
        },
        {
          name: "interview_prep",
          description: "基于笔记内容,为指定话题准备面试答案",
          arguments: [
            {
              name: "topic",
              description: "面试话题,如 'Agent架构设计'",
              required: true,
            },
          ],
        },
      ],
    };
  });

  /**
   * 获取 Prompt 内容
   */
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    switch (name) {
      case "summarize_day": {
        const dayNumber = args?.day_number || "1";
        return {
          messages: [
            {
              role: "user" as const,
              content: {
                type: "text" as const,
                text:
                  `请先使用 search_notes 工具搜索 "Day ${dayNumber}" 相关的笔记,` +
                  `然后使用 get_note_content 获取完整内容,最后按以下格式总结:\n\n` +
                  `## Day ${dayNumber} 学习总结\n\n` +
                  `### 核心概念(3-5个要点)\n` +
                  `### 关键代码/命令\n` +
                  `### 面试可能问到的问题\n` +
                  `### 与其他Day的关联\n` +
                  `### 一句话总结`,
              },
            },
          ],
        };
      }

      case "compare_concepts": {
        const a = args?.concept_a || "概念A";
        const b = args?.concept_b || "概念B";
        return {
          messages: [
            {
              role: "user" as const,
              content: {
                type: "text" as const,
                text:
                  `请分别搜索"${a}"和"${b}"相关的笔记,然后按以下格式对比:\n\n` +
                  `## ${a} vs ${b} 对比分析\n\n` +
                  `| 维度 | ${a} | ${b} |\n` +
                  `|------|------|------|\n` +
                  `| 核心原理 | | |\n` +
                  `| 适用场景 | | |\n` +
                  `| 优势 | | |\n` +
                  `| 劣势 | | |\n` +
                  `| 成本 | | |\n\n` +
                  `### 什么时候用 ${a}?\n` +
                  `### 什么时候用 ${b}?\n` +
                  `### 能否结合使用?`,
              },
            },
          ],
        };
      }

      case "interview_prep": {
        const topic = args?.topic || "AI";
        return {
          messages: [
            {
              role: "user" as const,
              content: {
                type: "text" as const,
                text:
                  `请搜索"${topic}"相关的笔记,然后帮我准备面试答案:\n\n` +
                  `## ${topic} 面试准备\n\n` +
                  `### 30秒版本(电梯话术)\n` +
                  `### 2分钟详细版本\n` +
                  `### 可能的追问及答案\n` +
                  `### 实际案例/项目经验\n` +
                  `### 需要注意的"坑"`,
              },
            },
          ],
        };
      }

      default:
        throw new Error(`Unknown prompt: ${name}`);
    }
  });
}

多 Tool 组合与流式响应 / Multi-Tool & Streaming

/**
 * 高级工具:笔记分析(组合多个基础能力)
 *
 * 这个工具展示了如何在 MCP Server 中
 * 实现复杂的多步骤逻辑
 */

// 在 ListToolsRequestSchema 中添加:
{
  name: "analyze_learning_progress",
  description:
    "分析学习进度和知识覆盖度。" +
    "扫描所有笔记,统计各阶段完成情况、知识点覆盖、" +
    "和建议下一步学习方向。",
  inputSchema: {
    type: "object" as const,
    properties: {
      stage: {
        type: "string",
        description: "可选,只分析指定阶段,如 'phase1', 'phase2'",
        enum: ["phase1", "phase2", "phase3", "phase4", "phase5", "all"],
      },
    },
  },
}

// 在 CallToolRequestSchema 中添加:
case "analyze_learning_progress": {
  const notes = scanNotes();
  const stage = (args as { stage?: string }).stage || "all";

  // 按阶段分组
  const phases: Record<string, NoteInfo[]> = {
    phase1: notes.filter(n => n.day >= 1 && n.day <= 15),
    phase2: notes.filter(n => n.day >= 16 && n.day <= 30),
    phase3: notes.filter(n => n.day >= 31 && n.day <= 42),
    phase4: notes.filter(n => n.day >= 43 && n.day <= 50),
    phase5: notes.filter(n => n.day >= 51 && n.day <= 60),
  };

  const phaseNames: Record<string, string> = {
    phase1: "模型基础 (Day 1-15)",
    phase2: "工程实践 (Day 16-30)",
    phase3: "金融零售AI应用 (Day 31-42)",
    phase4: "面试冲刺 (Day 43-50)",
    phase5: "动手实战 (Day 51-60)",
  };

  const phaseExpected: Record<string, number> = {
    phase1: 15, phase2: 15, phase3: 12, phase4: 8, phase5: 10,
  };

  let report = "# 学习进度分析报告\n\n";

  const targetPhases = stage === "all"
    ? Object.keys(phases)
    : [stage];

  for (const p of targetPhases) {
    const completed = phases[p]?.length || 0;
    const expected = phaseExpected[p] || 0;
    const pct = expected > 0 ? Math.round((completed / expected) * 100) : 0;
    const bar = "█".repeat(Math.round(pct / 5)) + "░".repeat(20 - Math.round(pct / 5));

    report += `## ${phaseNames[p]}\n`;
    report += `进度: [${bar}] ${pct}% (${completed}/${expected})\n`;

    if (phases[p] && phases[p].length > 0) {
      report += `已完成:\n`;
      for (const note of phases[p]) {
        report += `  - Day ${note.day}: ${note.title}\n`;
      }
    }
    report += "\n";
  }

  // 总体统计
  const totalCompleted = notes.length;
  const totalExpected = 60;
  report += `## 总体进度\n`;
  report += `已完成 ${totalCompleted}/${totalExpected} 天 (${Math.round(totalCompleted / totalExpected * 100)}%)\n`;
  report += `笔记总大小: ${Math.round(notes.reduce((s, n) => s + n.size, 0) / 1024)} KB\n`;

  return {
    content: [{ type: "text" as const, text: report }],
  };
}

错误处理最佳实践 / Error Handling Best Practices

/**
 * MCP Server 的错误处理
 *
 * Day 17 学的安全与护栏 + Day 22 的错误恢复
 * → 在 MCP Server 中的实践
 */

// 1. 输入验证
function validateFilename(filename: string): boolean {
  // 防止路径遍历攻击
  if (filename.includes("..") || filename.includes("/") || filename.includes("\\")) {
    return false;
  }
  // 只允许 .md 文件
  if (!filename.endsWith(".md")) {
    return false;
  }
  return true;
}

// 2. 在工具调用中使用
case "get_note_content": {
  const filename = (args as { filename: string }).filename;

  if (!validateFilename(filename)) {
    return {
      content: [{
        type: "text" as const,
        text: `无效的文件名: "${filename}"。文件名只能包含字母、数字、连字符,且必须以 .md 结尾。`,
      }],
      isError: true,
    };
  }
  // ... 正常处理
}

// 3. 全局错误处理
server.onerror = (error: Error) => {
  console.error("[MCP Server Error]", error.message);
  // 不要崩溃,记录错误继续运行
};

// 4. 超时保护
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  return Promise.race([
    promise,
    new Promise<T>((_, reject) =>
      setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)
    ),
  ]);
}

知识点4:与 Claude Code 集成 / Claude Code Integration

配置 MCP Server / Configuring MCP Server

// .claude/settings.local.json (项目级配置)
// 或 ~/.claude/settings.json (全局配置)

{
  "mcpServers": {
    "notes-server": {
      "command": "node",
      "args": ["E:/code/momofinance/momoweb3/mcp-notes-server/dist/index.js"],
      "env": {
        "NOTES_DIR": "E:/code/momofinance/momoweb3/docs/ai"
      }
    }
  }
}

构建并测试 / Build & Test

# === Step 1: 构建 TypeScript ===
cd mcp-notes-server
npm run build

# === Step 2: 本地测试 (直接运行) ===
# MCP Server 通过 stdio 通信
# 手动测试需要发送 JSON-RPC 消息

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js

# === Step 3: 在 Claude Code 中测试 ===
# 重启 Claude Code,它会自动连接配置的 MCP Server

# 然后你可以这样用:
# "搜索我笔记中关于 LoRA 的内容"
# → Claude Code 会自动调用 search_notes 工具

# "读取 Day 7 的完整笔记"
# → Claude Code 会调用 get_note_content 工具

# "帮我总结 Day 51-55 的实战收获"
# → Claude Code 会多次调用工具,综合生成总结

实际效果展示 / Demo in Claude Code

在 Claude Code 中的对话效果:

用户: "我之前学的RAG系统,有哪些关键指标?"

Claude Code:
  → 调用 search_notes("RAG 评估 指标")
  → 找到 day21-production-rag-evaluation.md, day53-hands-on-rag-optimization.md
  → 调用 get_note_content("day21-production-rag-evaluation.md", section: "评估")
  → 整合信息后回答:

"根据你 Day 21 和 Day 53 的笔记,RAG 系统的关键评估指标是 RAGAS 框架的四个维度:
1. Faithfulness(忠实度)— 回答是否忠于检索到的上下文
2. Answer Relevancy(回答相关性)— 回答是否与问题相关
3. Context Precision(上下文精度)— 检索到的内容是否精确相关
4. Context Recall(上下文召回)— 是否检索到了所有相关内容

在 Day 53 的实战中,你通过 Golden QA 集做了基线评估..."

这就是 MCP 的价值:
  Claude Code 能直接访问你的个人知识库
  不需要手动复制粘贴笔记内容
  AI 助手变成了"懂你"的助手

知识点5:Web3 MCP Server / Web3 Data MCP Server

链上数据查询 Server / On-chain Data Query Server

/**
 * src/web3-server.ts
 * Web3 MCP Server — 查询链上数据
 *
 * 提供的工具:
 * - get_eth_price: 查询 ETH 实时价格
 * - get_token_balance: 查询地址的 Token 余额
 * - get_gas_price: 查询当前 Gas 价格
 *
 * 结合 Day 55 的 Web3 Agent 工具
 * → 封装成 MCP Server 让任何 AI 客户端使用
 */

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

// ==========================================
// API 配置
// ==========================================

const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || "";
const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY || "";

// ==========================================
// HTTP 请求工具
// ==========================================

async function fetchJSON(url: string): Promise<any> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 10000);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return await response.json();
  } finally {
    clearTimeout(timeout);
  }
}

// ==========================================
// 创建 Web3 MCP Server
// ==========================================

const server = new Server(
  { name: "mcp-web3-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// ==========================================
// 注册 Tools
// ==========================================

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "get_eth_price",
        description:
          "获取 ETH 的实时价格和市场数据。" +
          "返回当前价格(USD)、24h变化、市值等。" +
          "数据来源: CoinGecko API。",
        inputSchema: {
          type: "object" as const,
          properties: {
            currency: {
              type: "string",
              description: "价格货币单位,默认 'usd'",
              default: "usd",
            },
          },
        },
      },
      {
        name: "get_token_balance",
        description:
          "查询以太坊地址的 ETH 余额和主要 ERC20 代币余额。" +
          "输入以太坊地址(0x开头),返回余额信息。" +
          "需要 ETHERSCAN_API_KEY 环境变量。",
        inputSchema: {
          type: "object" as const,
          properties: {
            address: {
              type: "string",
              description: "以太坊地址,如 '0x...'",
            },
          },
          required: ["address"],
        },
      },
      {
        name: "get_gas_price",
        description:
          "获取以太坊当前 Gas 价格。" +
          "返回 Safe/Proposed/Fast 三档 Gas 价格(Gwei)。" +
          "帮助判断当前是否适合发送交易。",
        inputSchema: {
          type: "object" as const,
          properties: {},
        },
      },
      {
        name: "get_protocol_tvl",
        description:
          "获取 DeFi 协议的 TVL(总锁仓量)。" +
          "输入协议名称(如 'uniswap', 'aave', 'lido')," +
          "返回当前 TVL、链分布等数据。数据来源: DeFiLlama。",
        inputSchema: {
          type: "object" as const,
          properties: {
            protocol: {
              type: "string",
              description: "协议名称,如 'uniswap', 'aave', 'lido'",
            },
          },
          required: ["protocol"],
        },
      },
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case "get_eth_price": {
        const currency = (args as { currency?: string }).currency || "usd";
        const data = await fetchJSON(
          `https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=${currency}&include_24hr_change=true&include_market_cap=true&include_24hr_vol=true`
        );

        const eth = data.ethereum;
        const result = [
          `ETH 实时价格:`,
          `  价格: $${eth[currency]?.toLocaleString()}`,
          `  24h 变化: ${eth[`${currency}_24h_change`]?.toFixed(2)}%`,
          `  市值: $${(eth[`${currency}_market_cap`] / 1e9)?.toFixed(2)}B`,
          `  24h 交易量: $${(eth[`${currency}_24h_vol`] / 1e9)?.toFixed(2)}B`,
          `  数据时间: ${new Date().toISOString()}`,
        ].join("\n");

        return { content: [{ type: "text" as const, text: result }] };
      }

      case "get_token_balance": {
        const address = (args as { address: string }).address;

        // 验证地址格式
        if (!address.match(/^0x[a-fA-F0-9]{40}$/)) {
          return {
            content: [{
              type: "text" as const,
              text: `无效的以太坊地址格式: ${address}`,
            }],
            isError: true,
          };
        }

        if (!ETHERSCAN_API_KEY) {
          return {
            content: [{
              type: "text" as const,
              text: "需要配置 ETHERSCAN_API_KEY 环境变量才能查询余额。",
            }],
            isError: true,
          };
        }

        const data = await fetchJSON(
          `https://api.etherscan.io/api?module=account&action=balance&address=${address}&tag=latest&apikey=${ETHERSCAN_API_KEY}`
        );

        if (data.status !== "1") {
          return {
            content: [{
              type: "text" as const,
              text: `查询失败: ${data.message}`,
            }],
            isError: true,
          };
        }

        const balanceWei = BigInt(data.result);
        const balanceEth = Number(balanceWei) / 1e18;

        const result = [
          `地址: ${address}`,
          `ETH 余额: ${balanceEth.toFixed(6)} ETH`,
          `Wei: ${balanceWei.toString()}`,
        ].join("\n");

        return { content: [{ type: "text" as const, text: result }] };
      }

      case "get_gas_price": {
        if (!ETHERSCAN_API_KEY) {
          // 使用公开 API 作为后备
          const data = await fetchJSON(
            `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${ETHERSCAN_API_KEY || "YourApiKeyToken"}`
          );

          if (data.status === "1") {
            const gas = data.result;
            const result = [
              `以太坊当前 Gas 价格:`,
              `  Safe (慢):     ${gas.SafeGasPrice} Gwei`,
              `  Proposed (中): ${gas.ProposeGasPrice} Gwei`,
              `  Fast (快):     ${gas.FastGasPrice} Gwei`,
              `  Base Fee:      ${gas.suggestBaseFee} Gwei`,
              ``,
              `费用估算 (ETH Transfer, 21000 gas):`,
              `  Safe:     ~${(Number(gas.SafeGasPrice) * 21000 / 1e9).toFixed(6)} ETH`,
              `  Fast:     ~${(Number(gas.FastGasPrice) * 21000 / 1e9).toFixed(6)} ETH`,
              ``,
              `建议: ${Number(gas.ProposeGasPrice) < 20 ? "Gas 较低,适合交易" : "Gas 较高,非紧急交易可以等等"}`,
            ].join("\n");

            return { content: [{ type: "text" as const, text: result }] };
          }
        }

        return {
          content: [{
            type: "text" as const,
            text: "无法获取 Gas 价格。请配置 ETHERSCAN_API_KEY。",
          }],
        };
      }

      case "get_protocol_tvl": {
        const protocol = (args as { protocol: string }).protocol;
        const data = await fetchJSON(
          `https://api.llama.fi/protocol/${protocol}`
        );

        const currentTvl = data.currentChainTvls || {};
        const totalTvl = Object.entries(currentTvl)
          .filter(([k]) => !k.endsWith("-staking") && !k.endsWith("-borrowed"))
          .reduce((sum, [, v]) => sum + (v as number), 0);

        // Top 5 chains by TVL
        const topChains = Object.entries(currentTvl)
          .filter(([k]) => !k.endsWith("-staking") && !k.endsWith("-borrowed"))
          .sort(([, a], [, b]) => (b as number) - (a as number))
          .slice(0, 5);

        const result = [
          `协议: ${data.name || protocol}`,
          `类别: ${data.category || "N/A"}`,
          `总 TVL: $${(totalTvl / 1e9).toFixed(2)}B`,
          ``,
          `链分布 (Top 5):`,
          ...topChains.map(([chain, tvl]) =>
            `  ${chain}: $${((tvl as number) / 1e6).toFixed(1)}M (${((tvl as number) / totalTvl * 100).toFixed(1)}%)`
          ),
          ``,
          `简介: ${(data.description || "").slice(0, 200)}`,
        ].join("\n");

        return { content: [{ type: "text" as const, text: result }] };
      }

      default:
        return {
          content: [{ type: "text" as const, text: `未知工具: ${name}` }],
          isError: true,
        };
    }
  } catch (error: any) {
    return {
      content: [{
        type: "text" as const,
        text: `工具执行错误: ${error.message}`,
      }],
      isError: true,
    };
  }
});

// ==========================================
// 启动
// ==========================================

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Web3 MCP Server running");
}

main().catch(console.error);

Claude Code 配置双 Server / Configuring Both Servers

{
  "mcpServers": {
    "notes-server": {
      "command": "node",
      "args": ["E:/code/momofinance/momoweb3/mcp-notes-server/dist/index.js"],
      "env": {
        "NOTES_DIR": "E:/code/momofinance/momoweb3/docs/ai"
      }
    },
    "web3-server": {
      "command": "node",
      "args": ["E:/code/momofinance/momoweb3/mcp-web3-server/dist/index.js"],
      "env": {
        "ETHERSCAN_API_KEY": "your-key-here",
        "ALCHEMY_API_KEY": "your-key-here"
      }
    }
  }
}

知识点6:发布与分享 / Publishing & Sharing

npm 发布流程 / npm Publishing

# === Step 1: 准备发布 ===
cd mcp-notes-server

# 确保构建成功
npm run build

# 添加 shebang 到入口文件
# 在 dist/index.js 开头添加: #!/usr/bin/env node

# === Step 2: 更新 package.json ===
# 确保以下字段正确:
#   "name": "@your-scope/mcp-notes-server"  (带 scope 避免命名冲突)
#   "version": "1.0.0"
#   "main": "dist/index.js"
#   "bin": { "mcp-notes-server": "dist/index.js" }
#   "files": ["dist/"]  (只发布编译后的文件)

# === Step 3: 登录 npm ===
npm login

# === Step 4: 发布 ===
npm publish --access public

# === Step 5: 验证 ===
# 别人可以通过以下方式安装和使用:
npx @your-scope/mcp-notes-server

README 编写 / Writing README

# MCP Notes Server

A Model Context Protocol server that lets AI assistants search
and read your learning notes.

## Features

- **search_notes**: Full-text search across all notes
- **get_note_content**: Read complete note contents
- **notes://list**: Browse all available notes

## Quick Start

### Install

\`\`\`bash
npm install -g @your-scope/mcp-notes-server
\`\`\`

### Configure with Claude Code

Add to `.claude/settings.local.json`:

\`\`\`json
{
  "mcpServers": {
    "notes-server": {
      "command": "npx",
      "args": ["@your-scope/mcp-notes-server"],
      "env": {
        "NOTES_DIR": "/path/to/your/notes"
      }
    }
  }
}
\`\`\`

### Usage

In Claude Code, you can now:

- "Search my notes for LoRA fine-tuning"
- "Read my Day 7 learning notes"
- "Summarize what I learned about RAG"

## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| NOTES_DIR | Path to notes directory | ./docs/ai |

## License

MIT

MCP Server Registry / 注册到社区

MCP Server 可以注册到社区发现平台:

1. Anthropic MCP Servers 列表
   https://github.com/modelcontextprotocol/servers
   提交 PR 添加你的 Server

2. MCP Hub
   社区维护的 MCP Server 目录
   提交你的 Server 信息

3. npm 搜索
   用 "mcp-server" 作为关键词
   其他人搜索 MCP Server 时能找到

发布清单:
  ✅ 代码在 GitHub 公开
  ✅ npm 包已发布
  ✅ README 清晰(安装、配置、使用)
  ✅ 包含使用示例
  ✅ 指定 MIT 或 Apache 许可证

版本管理 / Version Management

语义版本控制 (SemVer):

1.0.0 → 1.0.1 (patch)
  修复 bug,不影响现有功能
  例: 修复文件编码问题

1.0.0 → 1.1.0 (minor)
  添加新工具或功能,向后兼容
  例: 新增 analyze_learning_progress 工具

1.0.0 → 2.0.0 (major)
  破坏性变更,不向后兼容
  例: 改变工具的输入参数格式

发布新版本:
  # 1. 修改 package.json 版本号
  npm version patch  # 或 minor / major

  # 2. 构建
  npm run build

  # 3. 发布
  npm publish

  # 4. 打 Git tag
  git tag v1.0.1
  git push --tags

最佳实践:
  - 每次发布前在本地测试
  - 写 CHANGELOG.md 记录变更
  - 重要变更发布前先发 beta 版
    npm publish --tag beta

今日思考 / Today's Reflections

思考1:标准化的力量 / The Power of Standardization

Day 55 写的 Agent 工具 vs Day 56 的 MCP Server
功能完全一样,但价值截然不同

Agent 工具:
  价值 = 工具本身的功能
  受众 = 只有你的 Agent
  维护 = 框架升级可能要改

MCP Server:
  价值 = 工具功能 × 能使用的客户端数量
  受众 = Claude Code, Cursor, Windsurf, 任何 MCP 客户端
  维护 = 协议稳定,客户端升级不影响

标准化协议的价值:
  USB 的出现让所有设备能用同一个接口
  HTTP 的出现让所有浏览器能访问任何网站
  MCP 的出现让所有 AI 应用能使用任何工具

Day 13 学 MCP 时觉得"又一个协议"
今天实操后理解了:
  这不是"又一个协议"
  这是 AI 工具的"USB标准"

思考2:开发者体验决定生态 / DX Determines Ecosystem

MCP SDK 的设计让开发变得很简单:

  1. 定义工具 schema → ListToolsRequestSchema
  2. 实现工具逻辑 → CallToolRequestSchema
  3. 启动 Server → StdioServerTransport

整个过程不到 200 行代码就能实现一个可用的 Server

对比其他方案:
  - 写 REST API → 需要 Express/路由/中间件/部署
  - 写 LangChain Tool → 只能在 LangChain 生态中用
  - 写 ChatGPT Plugin → OpenAI 已经放弃了

MCP 胜出的原因不是技术最先进
而是开发者体验最好 + Anthropic 的推动

PM 视角的启示:
  "好用"比"强大"更重要
  降低开发者门槛 = 加速生态建设
  一个人3小时能写出一个 MCP Server = 生态爆发的基础

思考3:MCP 是 AI PM 的新赛道 / MCP as a New Track for AI PMs

MCP 生态创造了新的产品机会:

1. MCP Server 即产品
   不需要 UI,不需要前端
   一个好用的 MCP Server = 一个有价值的产品
   例: 今天写的 Web3 数据 Server

2. MCP Marketplace
   类似 App Store 但针对 AI 工具
   PM 可以策划"AI 工具商店"

3. 企业 MCP 平台
   企业内部工具封装成 MCP Server
   让 AI 助手能访问企业数据
   PM 可以设计"企业 AI 工具平台"

4. MCP Server 编排
   多个 Server 组合使用
   PM 可以设计"工具编排平台"

对于 Web3 PM 来说:
  链上数据 MCP Server → 让任何 AI 都能查询链上数据
  DeFi 操作 MCP Server → 让 AI 能执行 DeFi 操作
  治理 MCP Server → 让 AI 能参与 DAO 治理

这是 Day 81 学的 "MCP+Web3" 的实际落地!

学习资源 / Resources

MCP 官方

教程

API 参考

优秀 MCP Server 示例


明日预告 / Tomorrow's Preview

Day 57: 多模态应用 — 图文理解与文档分析

从文本到视觉:
  Day 51-56: 处理的都是文本
  Day 57: 开始处理图片和文档

Day 10 学的多模态理论 → Day 57 的实际应用

明天将实现:
1. 图片理解:用 LLaVA 模型分析截图
2. 文档分析:PDF/PPT → 结构化信息提取
3. OCR + LLM:图片中的文字 → 智能理解
4. 实际场景:分析架构图、理解白皮书

准备工作:
  ollama pull llava:7b     # 下载多模态模型
  pip install pymupdf      # PDF 处理
  pip install Pillow       # 图片处理

从"读文字"到"看图片",AI 能力再次升级!

Day 56 完成! 从零开发了两个 MCP Server:笔记搜索和 Web3 数据查询。 一次开发,Claude Code / Cursor / 任何 MCP 客户端都能使用。 明天进入多模态领域,让 AI 不仅能"读"还能"看"!