Contents

MCP 工具测试

项目结构概览

1
2
3
4
5
6
7
8
mcp-data-analyst/
├── .venv/                 # 虚拟环境
├── data/                  # 存放待分析的 CSV 文件(需手动创建)
├── store.py               # 数据存储层
├── mcp_server.py          # MCP 工具定义
├── app.py                 # FastAPI + MCP 挂载
├── requirements.txt       # 依赖:mcp[cli], fastapi, uvicorn, pandas
└── data-analysis-skill.md # Skill 指南

第一步:环境准备

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 创建项目目录
mkdir mcp-data-analyst && cd mcp-data-analyst

# 创建虚拟环境
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate

pip install "mcp[cli]" fastapi uvicorn
pip freeze > requirements.txt
pip install -r requirements.txt

官方 SDK 的核心是 mcp

第二步:创建数据存储层 (store.py)

先准备一个简单的数据存储,用来管理待分析的数据文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# store.py
import os
import pandas as pd
from pathlib import Path
from typing import Optional, Dict, Any


class DataStore:
    """本地数据存储管理器"""

    def __init__(self, data_dir: str = "./data"):
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(exist_ok=True)
        self._datasets: Dict[str, pd.DataFrame] = {}

    def list_files(self) -> list:
        """列出数据目录下的所有文件"""
        return [f.name for f in self.data_dir.iterdir() if f.is_file()]

    def load_csv(self, filename: str) -> Optional[Dict[str, Any]]:
        """加载 CSV 文件并返回基本信息"""
        filepath = self.data_dir / filename
        if not filepath.exists():
            return None

        df = pd.read_csv(filepath)
        self._datasets[filename] = df

        return {
            "filename": filename,
            "rows": len(df),
            "columns": list(df.columns),
            "preview": df.head(5).to_dict(orient="records"),
        }

    def execute_query(self, filename: str, query: str) -> Optional[Dict]:
        """在指定数据集上执行 pandas 查询"""
        if filename not in self._datasets:
            self.load_csv(filename)

        df = self._datasets.get(filename)
        if df is None:
            return None

        try:
            # 安全执行查询(生产环境需要更严格的沙箱)
            result = df.eval(query)
            if isinstance(result, pd.Series):
                result = result.to_dict()
            elif isinstance(result, pd.DataFrame):
                result = result.head(100).to_dict(orient="records")
            return {"result": result, "success": True}
        except Exception as e:
            return {"error": str(e), "success": False}


# 全局实例
store = DataStore()

第三步:定义 MCP 工具 (mcp_server.py)

这是核心部分——用官方 SDK 的 FastMCP 定义工具
关键点:设置 stateless_http=True 启用生产级无状态模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# mcp_server.py
from mcp.server.fastmcp import FastMCP
from store import store

# 创建 MCP 服务器,启用无状态 HTTP 模式(生产部署关键)
mcp = FastMCP("LocalDataAnalyst", stateless_http=True)


# ========== 工具 1:列出数据文件 ==========
@mcp.tool()
async def list_data_files() -> list:
    """列出数据目录下的所有文件"""
    return store.list_files()


# ========== 工具 2:加载并预览 CSV ==========
@mcp.tool()
async def preview_csv(filename: str) -> dict:
    """加载 CSV 文件并返回预览(前5行、列名、行数)

    Args:
        filename: CSV 文件名(需位于 ./data 目录下)
    """
    result = store.load_csv(filename)
    if result is None:
        return {"error": f"文件 {filename} 不存在"}
    return result


# ========== 工具 3:执行数据分析查询 ==========
@mcp.tool()
async def analyze_data(filename: str, query: str) -> dict:
    """在已加载的数据集上执行分析查询

    Args:
        filename: CSV 文件名
        query: pandas 查询表达式,例如 "df['sales'].mean()" 或 "df[df['amount'] > 100]"

    示例查询:
        - "df['revenue'].mean()"
        - "df.groupby('category')['sales'].sum()"
        - "df[df['quantity'] > 10]"
    """
    return store.execute_query(filename, query)


# ========== 工具 4:获取数据摘要统计 ==========
@mcp.tool()
async def get_summary_stats(filename: str, column: str = None) -> dict:
    """获取数据的描述性统计

    Args:
        filename: CSV 文件名
        column: 可选,指定列名;不指定则返回所有数值列的统计
    """
    result = store.load_csv(filename)
    if result is None:
        return {"error": f"文件 {filename} 不存在"}

    # 重新获取 DataFrame(store 已缓存)
    import pandas as pd

    df = pd.read_csv(store.data_dir / filename)

    if column:
        if column not in df.columns:
            return {"error": f"列 {column} 不存在"}
        stats = df[column].describe().to_dict()
    else:
        stats = df.describe().to_dict()

    return {"filename": filename, "statistics": stats}


if __name__ == "__main__":
    mcp.run(transport="stdio")  # stdio 模式用于本地测试
    # mcp dev mcp_server.py

每个工具函数的文档字符串(docstring)和参数类型注解非常重要——AI 模型会读取它们来理解工具的作用和用法

第四步:Python 原生调试

创建 test_client.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# test_client.py
import asyncio
import json
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def test_tools():
    # 连接到你的 MCP 服务器
    server_params = StdioServerParameters(
        command="python",
        args=["mcp_server.py"],  # 你的服务器文件
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # 列出所有工具
            tools = await session.list_tools()
            print("可用工具:")
            for tool in tools.tools:
                print(f"  - {tool.name}: {tool.description[:50]}...")

            # 测试 list_data_files 工具
            print("\n测试 list_data_files:")
            result = await session.call_tool("list_data_files", arguments={})
            print(f"结果: {result.model_dump_json()}")

            print("\n测试 preview_csv")
            result = await session.call_tool(
                "preview_csv", arguments={"filename": "test.csv"}
            )
            print(f"结果: {result.model_dump_json()}")

            print("\n测试 analyze_data")
            result = await session.call_tool(
                "analyze_data",
                arguments={"filename": "test.csv", "query": "total = sales * quantity"},
            )
            print(f"结果: {result.model_dump_json()}")

            print("\n测试 get_summary_stats")
            result = await session.call_tool(
                "get_summary_stats", arguments={"filename": "test.csv"}
            )
            print(f"结果: {result.model_dump_json()}")


if __name__ == "__main__":
    asyncio.run(test_tools())

第五步:编写 Skill(操作流程指南)

在项目目录下创建 data-analysis-skill.md

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
---
name: local-data-analysis
description: 分析本地 CSV 数据文件
---

# 本地数据分析技能

## 工作流程

1. **列出可用文件**:调用 `list_data_files` 查看数据目录下有哪些 CSV 文件
2. **预览文件**:调用 `preview_csv` 加载文件并查看前 5 行和列信息
3. **理解需求**:根据用户的问题,确定分析目标
4. **执行分析**   - 简单统计:用 `get_summary_stats` 获取描述性统计
   - 自定义分析:用 `analyze_data` 执行 pandas 表达式
5. **解释结果**:用自然语言向用户解释分析结果

## 查询示例

当用户问"销售额最高的前 5 个产品是什么",你应该:

1. 调用 `preview_csv` 了解数据结构
2. 调用 `analyze_data`,query 参数设为 `"df.nlargest(5, 'sales')[['product', 'sales']]"`
3. 将返回的结果整理成表格或列表

## 注意事项

- 确保查询表达式使用 `df` 作为 DataFrame 变量名
- 复杂查询建议先小范围测试

其他

若需要交互式地测试你的工具:
创建 FastAPI 应用并挂载 MCP (app.py)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# app.py
from contextlib import AsyncExitStack, asynccontextmanager
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from mcp_server import mcp


@asynccontextmanager
async def lifespan(app: FastAPI):
    """管理 MCP 会话生命周期"""
    async with AsyncExitStack() as stack:
        await stack.enter_async_context(mcp.session_manager.run())
        yield


app = FastAPI(lifespan=lifespan)

# 将 MCP 服务器挂载到根路径
app.mount("/", mcp.streamable_http_app())


# 健康检查端点(用于容器部署)
@app.get("/health")
async def health():
    return JSONResponse({"status": "healthy"})


# 可选:添加一个简单的 Web 界面提示
@app.get("/")
async def root():
    return {
        "message": "MCP Data Analyst Server is running",
        "mcp_endpoint": "/mcp",
        "tools": [
            "list_data_files",
            "preview_csv",
            "analyze_data",
            "get_summary_stats",
        ],
    }

# uvicorn app:app --reload --port 7891

官方 SDK 的 streamable_http_app() 方法返回一个标准的 FastAPI 应用,MCP 端点默认在 /mcp 子路径

  • 测试服务器

    1
    2
    
    # 启动服务器
    uvicorn app:app --reload --port 8080
    

    你应该看到类似这样的输出:

    1
    2
    
    INFO:     Uvicorn running on http://127.0.0.1:8080
    INFO:     Application startup complete.
    
  • 用 MCP Inspector 测试(官方推荐)

    1
    
    mcp dev mcp_server.py
    

    这会打开一个 Web 界面,你可以:

    • 查看所有已注册的工具
    • 手动调用工具并查看返回结果
    • 调试参数传递