MCP 实战——压轴项目:AI 塔罗神谕

223 阅读14分钟

你做到了!欢迎来到《Practical MCP:Python 开发者指南》的最后一章

在过去的七章里,你走过了一段精彩旅程:
第 1 章从宏观出发,理解了为什么 MCP 会成为 AI 的游戏规则改变者;
第 2 章完成了你的 “Hello, World”,构建了第一个服务器;
第 3 章把工具接上了真实世界,集成到主流 LLM 平台;
第 4 章深入 核心 MCP 组件,掌握了 Resources、Prompts 和强大的 Context 对象;
第 5 章学习了如何用 认证与部署 将项目产品化;
第 6 章探索 高级服务器架构,把 MCP 集成进标准 Web 应用;
第 7 章则巡礼了社区生态的 Community Spotlights

你已经具备了所有技能,也见过所有拼图。现在,是时候把它们拼在一起了。

本章是你的压轴项目。你将从零搭建一个端到端应用,展示 Model Context Protocol 的力量与优雅。我们不是在做一个单一用途的小工具,而是一个包含用户界面、强大的 LLM 编排器、以及多台专用 MCP 服务器协同工作的多组件系统。

在本章结束时,你将构建出:

  • 一个全栈 AI 应用:AI Oracle Tarot(AI 塔罗神谕)网页应用
  • 两个独立的 MCP 微服务:一个用于抽牌,一个用于管理数据库
  • 一个智能编排器:使用 Anthropic Claude 执行复杂的多步骤流程
  • 一个友好的界面:用流行的 Gradio 库构建

这是对你知识的终极考验:把整本书所学,汇聚成一个令人印象深刻的应用。让我们一起创造点魔法吧。

项目概览:AI 神谕

我们的最终项目是一个 AI 驱动的塔罗读牌师。用户提出问题后,系统会完成一套三张牌(过去 / 现在 / 未来)的解读,将其结合问题语境进行诠释,并把整个读牌结果保存到数据库以便日后查看。

这听起来复杂,但借助 MCP 架构,我们可以把它拆分成清晰、易维护、彼此独立的组件:

  • 用户界面(app.py) :用 Gradio 构建的 Web 界面。接收用户问题并展示最终解读,还包括“历史记录”标签页查看过往读牌。
  • LLM 编排器(llm_orchestrator.py) :应用的大脑。接收用户问题,为 Claude 构造主提示(master prompt),并告知可用的 MCP 服务器。Claude 将自行决定调用哪些工具、以何种顺序来完成请求。
  • 塔罗抽牌服务器(mcp_tarot_server.py) :高度专用的 MCP 服务器。唯一职责是从标准塔罗牌组中随机抽取三张牌。这是一个纯粹、无状态函数以工具形式暴露的典范。
  • 数据库管理服务器(mcp_database_server.py) :另一个 MCP 服务器,专责与 SQLite 数据库交互。它提供一个工具来保存已完成的读牌。这展示了 MCP 如何为数据库这类有状态资源提供安全、结构化的访问接口。
  • 共享逻辑与数据(tarot_cards.py, db_utils.py) :普通 Python 文件,提供静态牌组以及数据库初始化/历史查询的辅助函数。

这种关注点分离正是 MCP 哲学的核心。每个组件只做一件事,并把它做到最好。LLM 充当指挥家,调度这些专用服务完成复杂任务。

我们从地基开始搭建。

打地基:数据与数据库

在构建服务器前,我们需要准备它们所依赖的数据与工具函数。

塔罗牌组

首先需要一副牌。创建 tarot_cards.py,其中包含一个常量:全部 78 张塔罗牌的列表。

图 122. tarot_cards.py

TAROT_DECK = [
    # Major Arcana
    "0 - The Fool", "I - The Magician", "II - The High Priestess", "III - The Empress",
    "IV - The Emperor", "V - The Hierophant", "VI - The Lovers", "VII - The Chariot",
    "VIII - Strength", "IX - The Hermit", "X - Wheel of Fortune", "XI - Justice",
    "XII - The Hanged Man", "XIII - Death", "XIV - Temperance", "XV - The Devil",
    "XVI - The Tower", "XVII - The Star", "XVIII - The Moon", "XIX - The Sun",
    "XX - Judgement", "XXI - The World",

    # Wands
    "Ace of Wands", "Two of Wands", "Three of Wands", "Four of Wands", "Five of Wands",
    "Six of Wands", "Seven of Wands", "Eight of Wands", "Nine of Wands", "Ten of Wands",
    "Page of Wands", "Knight of Wands", "Queen of Wands", "King of Wands",

    # Cups
    "Ace of Cups", "Two of Cups", "Three of Cups", "Four of Cups", "Five of Cups",
    "Six of Cups", "Seven of Cups", "Eight of Cups", "Nine of Cups", "Ten of Cups",
    "Page of Cups", "Knight of Cups", "Queen of Cups", "King of Cups",

    # Swords
    "Ace of Swords", "Two of Swords", "Three of Swords", "Four of Swords", "Five of Swords",
    "Six of Swords", "Seven of Swords", "Eight of Swords", "Nine of Swords", "Ten of Swords",
    "Page of Swords", "Knight of Swords", "Queen of Swords", "King of Swords",

    # Pentacles
    "Ace of Pentacles", "Two of Pentacles", "Three of Pentacles", "Four of Pentacles", "Five of Pentacles",
    "Six of Pentacles", "Seven of Pentacles", "Eight of Pentacles", "Nine of Pentacles", "Ten of Pentacles",
    "Page of Pentacles", "Knight of Pentacles", "Queen of Pentacles", "King of Pentacles"
]

这就是我们的静态“牌组真相来源”,供抽牌服务器导入使用。

数据库工具

接着准备管理 SQLite 数据库的辅助函数。这样数据库逻辑与服务器逻辑相互分离。创建 db_utils.py

图 123. db_utils.py

import sqlite3

DB_FILE = "tarot_readings.db"

def setup_database():
    conn = sqlite3.connect(DB_FILE)
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS readings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            timestamp TEXT NOT NULL,
            question TEXT NOT NULL,
            card_past TEXT NOT NULL,
            card_present TEXT NOT NULL,
            card_future TEXT NOT NULL,
            analysis TEXT NOT NULL
        )
    """)
    conn.commit()
    conn.close()
    print("Database setup complete.")

def get_history():
    conn = sqlite3.connect(DB_FILE)
    try:
        cursor = conn.cursor()
        cursor.execute("""
            SELECT timestamp, question, card_past, card_present, card_future, analysis 
            FROM readings 
            ORDER BY timestamp DESC
        """)
        history = cursor.fetchall()
        return [list(row) for row in history]
    except sqlite3.OperationalError:
        return []
    finally:
        conn.close()

这个文件提供两个函数:

  • setup_database():若表不存在则创建 readings 表;
  • get_history():获取过往读牌历史,Gradio 的“历史”标签会用到。

地基就绪,开始搭建专用的 MCP 服务器。

构建 MCP 服务器:神谕的“工具箱”

我们将用 fastmcp 构建两个独立的 MCP 服务器。每个都是只服务单一职责的微服务。

抽牌服务器

该服务器唯一的工作是抽出三张互不重复的牌。它是将一个简单、可复用的动作封装为 MCP 工具的完美示例。创建 mcp_tarot_server.py

图 124. mcp_tarot_server.py

import random
from fastmcp import FastMCP
from tarot_cards import TAROT_DECK
from typing import List

mcp = FastMCP("TarotCardDrawer")

@mcp.tool
def draw_three_cards() -> List[str]:
    """
    Draws three unique Tarot cards randomly for a Past, Present, Future spread.
    """
    return random.sample(TAROT_DECK, 3)

if __name__ == "__main__":
    print("Starting Tarot Card Drawing MCP Server...")
    print(f"Deck contains {len(TAROT_DECK)} cards.")
    mcp.run(transport="http", host="127.0.0.1", port=9001)

这与第 2 章内容应当非常熟悉:初始化 FastMCP、导入 TAROT_DECK,并用 @mcp.tool 定义 draw_three_cards。函数用 random.sample 确保三张牌互不重复。服务器监听 9001 端口。

数据库管理服务器

该服务器为数据库交互提供安全且结构化的方式。相较直接开放原始 SQL(风险较高),我们只暴露一个保存读牌的工具。此外,第 7 章所用的第三方数据库 MCP 工具 mcp-database-server 仅支持 STDIO 传输;而我们这里自建一个 HTTP 版本,接口更可控。创建 mcp_database_server.py

图 125. mcp_database_server.py

import sqlite3
from datetime import datetime
from fastmcp import FastMCP
import db_utils

DB_FILE = "tarot_readings.db"

mcp = FastMCP("SQLiteDBManager")

@mcp.tool
def save_tarot_reading(
    question: str, 
    card_past: str, 
    card_present: str, 
    card_future: str, 
    analysis: str
) -> str:
    try:
        conn = sqlite3.connect(DB_FILE)
        cursor = conn.cursor()
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        cursor.execute("""
            INSERT INTO readings (timestamp, question, card_past, card_present, card_future, analysis)
            VALUES (?, ?, ?, ?, ?, ?)
        """, (timestamp, question, card_past, card_present, card_future, analysis))
        
        conn.commit()
        conn.close()
        
        return "Reading was saved successfully to the database."
        
    except Exception as e:
        print(f"Database error: {e}")
        return f"Error: Failed to save the reading to the database. Reason: {e}"

if __name__ == "__main__":
    print("Setting up database for the MCP server...")
    db_utils.setup_database()
    
    print("Starting SQLite Database MCP Server on port 9002...")
    mcp.run(transport="http", host="127.0.0.1", port=9002)

这里定义了 save_tarot_reading 工具。注意函数签名的类型提示question: stranalysis: str 等):fastmcp 会据此自动生成工具模式(schema),LLM 会用它来理解所需参数。服务器监听 9002 端口。

两位专职“工人”已就绪,现在他们只差一个“经理”。

运筹帷幄:LLM 编排器

这正是 MCP 的魔力所在。我们将编写一个 Python 函数,它本身不亲自做事,而是指导 LLM 如何使用我们的 MCP 服务器完成任务。创建 llm_orchestrator.py

图 126. llm_orchestrator.py

import os
import anthropic

def run_tarot_flow(question: str) -> str:
    api_key = os.environ.get("ANTHROPIC_API_KEY")
    tarot_server_url = os.environ.get("TAROT_MCP_URL")
    db_server_url = os.environ.get("DATABASE_MCP_URL")

    if not all([api_key, tarot_server_url, db_server_url]):
        raise ValueError("Missing required environment variables: ANTHROPIC_API_KEY, TAROT_MCP_URL, DB_MCP_URL")

    client = anthropic.Anthropic(api_key=api_key)

    master_prompt = f"""
You are a mystical AI Tarot reader and data entry assistant. Your task is to perform a complete tarot reading for a user and record it using the tools provided.

Here is the user's question: "{question}"

Follow these steps precisely in order:

1.  **Draw the Cards:** Call the `draw_three_cards` tool from the `tarot_drawer` server to get three cards for a Past, Present, and Future spread.

2.  **Analyze the Reading:** Once you have the three cards, interpret them in the context of the user's question. Create an insightful and constructive analysis.

3.  **Save to Database:** Call the `save_tarot_reading` tool from the `database_manager` server. You must provide all the required arguments:
    - `question`: The user's original question.
    - `card_past`: The first card you drew.
    - `card_present`: The second card you drew.
    - `card_future`: The third card you drew.
    - `analysis`: Your concise analysis of the reading (under 1200 characters).

4.  **Present to User:** After successfully saving to the database, formulate a final, user-friendly response. This response should include:
    - The cards drawn for Past, Present, and Future.
    - Your detailed analysis of the reading.
    - A concluding confirmation like "This reading has been recorded for you."

Do not mention the tool names in your final response to the user. Just perform the actions and present the beautiful reading.
"""
    try:
        response = client.beta.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=2000,
            messages=[{"role": "user", "content":  master_prompt}],
            mcp_servers=[
                {
                    "type": "url",
                    "url": tarot_server_url,
                    "name": "tarot_drawer",
                },
                {
                    "type": "url",
                    "url": db_server_url,
                    "name": "database_manager",
                }
            ],
            extra_headers={
                "anthropic-beta": "mcp-client-2025-04-04"
            }
        )
        final_response = response.content[-1].text
        return final_response

    except Exception as e:
        print(f"An error occurred during the LLM orchestration: {e}")
        return f"A mystical interference occurred. Please try again. (Error: {e})"

来分析这段关键代码:

  • 环境变量:正如第 5 章所学,API Key 与 URL 这类配置应从环境变量读取。
  • 主提示(Master Prompt) :这是工具使用场景下优秀的提示工程范例。我们不是笼统地让 Claude “做个塔罗读牌”,而是给出清晰的编号步骤,指定每一步用哪个工具、需要哪些参数。这种结构化提示能显著提高 LLM 工作流的可靠性。
  • mcp_servers 参数:这是第 3 章首次见到的集成核心。我们向 Claude 传入正在运行的 MCP 服务器列表,并给它们起明确的名字tarot_drawerdatabase_manager)。LLM 将在“思考”期间据此决定要把工具调用发送到哪个服务器。
  • 响应处理:Claude 响应里的最后一个文本块,将是我们在第 4 步所要求的面向用户的解读。我们提取并返回它。

编排器已就位,接下来就只差一个用户可以交互的入口了。

用户界面:基于 Gradio 的前端

我们的前端将采用 Gradio。它是一款极其易用的库,可快速搭建面向机器学习与 AI 应用的简单 Web 界面。

先安装它:

> uv add gradio

现在创建最后一个文件 app.py,把所有组件串联起来。

图 127. app.py

import gradio as gr
import db_utils
import llm_orchestrator
import time

def get_llm_driven_reading(question: str, progress=gr.Progress()):
    if not question.strip():
        raise gr.Error("Please enter a question before drawing cards.")

    progress(0, desc="Starting your reading...")
    time.sleep(0.5)
    
    progress(0.2, desc="Connecting to the AI Oracle...")
    time.sleep(0.5)
    
    progress(0.4, desc="Drawing your cards...")
    time.sleep(0.5)
    
    progress(0.6, desc="Interpreting the cards...")
    
    final_analysis = llm_orchestrator.run_tarot_flow(question)
    
    progress(1.0, desc="Your reading is complete!")
    time.sleep(0.5)

    return final_analysis

def load_history():
    return db_utils.get_history()

with gr.Blocks(title="AI Oracle Tarot") as demo:
    gr.Markdown("# 🔮 AI Oracle Tarot Reading")
    gr.Markdown("The AI will draw cards, interpret them, and save the reading for you.")

    with gr.Tabs() as tabs:
        with gr.TabItem("New Reading", id=0):
            question_box = gr.Textbox(
                label="Your Question",
                placeholder="e.g., What should I focus on for career growth?",
                lines=2
            )
            draw_button = gr.Button("Consult the Oracle", variant="primary")
            gr.Markdown("### Your Reading from the Oracle")
            analysis_markdown = gr.Markdown(min_height=100)
        
        with gr.TabItem("History", id=1) as history_tab:
            history_df = gr.Dataframe(
                headers=["Timestamp", "Question", "Past", "Present", "Future", "Analysis"],
                datatype=["str", "str", "str", "str", "str", "str"],
                label="Past Readings",
                interactive=False,
                wrap=True,
                max_height=500,
                value=load_history 
            )

    draw_button.click(
        fn=get_llm_driven_reading,
        inputs=[question_box],
        outputs=[analysis_markdown],
    )

    history_tab.select(
        fn=load_history,
        inputs=None,
        outputs=history_df
    )

if __name__ == "__main__":
    db_utils.setup_database()
    print("\n--- Ensure you have set the following environment variables ---")
    print("ANTHROPIC_API_KEY, TAROT_MCP_URL, DATABASE_MCP_URL")
    print("--- Make sure both Python MCP servers are running in separate terminals ---\n")
    demo.launch()

上面的 Gradio 代码在 gr.Blocks 上下文中定义了 UI 组件,并包含两个标签页:

  • New Reading:包含一个输入问题的文本框、一个开始流程的按钮,以及一个用于展示结果的 Markdown 输出块。通过 draw_button.click 事件,把按钮与 get_llm_driven_reading 函数(进而调用我们的编排器)连接起来。
  • History:包含一个 gr.Dataframe,其数据来自 db_utils.get_history()。通过 history_tab.select 处理器,确保每次点击“历史”标签时都会刷新数据。

一切拼图都已就位。是时候让“神谕”苏醒了。

把一切连起来:运行神谕

这是见证奇迹的时刻。我们将运行系统的全部组件(需要开多个终端窗口)。

第 1 步:运行 MCP 服务器

先启动两个微服务(各占一个终端)。

终端 1:

> python .\mcp_tarot_server.py

你应能看到运行在 9001 端口的 TarotCardDrawer 服务器的 fastmcp 启动画面。

终端 2:

> python .\mcp_database_server.py

你会看到运行在 9002 端口的 SQLiteDBManager 服务器的启动信息。

保持这两个终端常开

第 2 步:用 Ngrok 暴露服务器

如第 3 章所述,云端的 LLM 无法直接访问 localhost。我们使用 ngrok 将本地服务器暴露到公网。因为有两个服务器,使用 ngrok.yml 配置即可一条命令启动双隧道。

找到你的 ngrok.yml

  • Windows:C:\Users\youruser\AppData\Local\ngrok\ngrok.yml
  • Linux:~/.config/ngrok/ngrok.yml
  • macOS:~/Library/Application Support/ngrok/ngrok.yml

编辑成如下内容(记得替换为你的 authtoken):

version: "3"

agent:
    authtoken: your auth token

tunnels:
    first:
        addr: 9001
        proto: http
    second:
        addr: 9002
        proto: http

终端 3: 启动 ngrok

> ngrok start --all

ngrok 会显示两个公网地址,类似:

Forwarding  https://22e5fb8826a5.ngrok-free.app -> http://localhost:9001
Forwarding  https://3310b2a3cfcf.ngrok-free.app -> http://localhost:9002

别关这个窗口。接下来会用到这两个公网 URL。务必记得在 URL 末尾追加 /mcp/

第 3 步:设置环境变量

打开终端 4,为 llm_orchestratorapp.py 设置环境变量。

Linux / macOS(Bash):

export ANTHROPIC_API_KEY="your-anthropic-api-key"
export TAROT_MCP_URL="https://22e5fb8826a5.ngrok-free.app/mcp/"
export DATABASE_MCP_URL="https://3310b2a3cfcf.ngrok-free.app/mcp/"

Windows(PowerShell):

$Env:ANTHROPIC_API_KEY = "your-anthropic-api-key"
$Env:TAROT_MCP_URL = "https://22e5fb8826a5.ngrok-free.app/mcp/"
$Env:DATABASE_MCP_URL = "https://3310b2a3cfcf.ngrok-free.app/mcp/"

重要提示:用你的真实 Anthropic API Key 以及上一步 ngrok 输出的两个公网 URL 替换占位文本。

第 4 步:启动 Gradio 应用

一切就绪。在终端 4(已设置环境变量的那个终端)启动主应用。

图 128. 终端 4

> python app.py

你会看到一些启动信息,随后出现:

* Running on local URL:  http://127.0.0.1:7860

在浏览器中打开该地址即可。祝占卜顺利!🔮

image.png

现在,来提问吧!

步骤 5:向神谕请教!

  • 你此时应该能看到 AI Oracle Tarot 界面。
  • 在输入框里键入你的问题,例如: “MCP(Model Context Protocol)明年会不会大火?”
  • 点击 “Consult the Oracle(请示神谕)” 按钮。
  • 稍等片刻。
  • 不一会儿,完整的解读就会出现!

image.png

现在,点击 History(历史) 标签。你会看到你的解读已整齐地存入数据库,随时可供你查看。

恭喜你!你已成功构建并运行了一个由 Model Context Protocol 驱动的、复杂的多组件 AI 应用。

接下来去哪儿?

你读完了本书,但你作为一名实战型 MCP 开发者的旅程才刚刚开始。你已经打下了坚实的基础,而这个压轴项目正是你新技能的最好证明。

你已经看到 MCP 如何实现清晰的关注点分离,让你构建出专用、可测试、可复用的 AI 工具;也见证了 LLM 不再只是文本生成器,而是真正的服务编排者

这个项目非常适合继续扩展。下面是一些基于本书所学的进阶点子:

  • 添加认证(第 5 章) :使用 JWT Bearer Token 保护你的 MCP 服务器,确保只有你的 Gradio 应用能调用它们。
  • 部署到云端(第 5 章) :用 Docker 将两个 MCP 服务器和 Gradio 应用容器化,把整套栈部署到 Google Cloud RunAWS 等平台。
  • 增加更多工具:新建一个 MCP 服务器,使用文本生成图像 API为每张塔罗牌生成插图,并让编排器调用它,使解读更具视觉效果。
  • 利用社区服务器(第 7 章) :集成文件系统服务器,不仅把每次解读保存到数据库,还将其以格式化 PDF 的形式存到磁盘。
  • 高级架构(第 6 章) :把两个 MCP 服务器合并为一个挂载式 ASGI 应用,以便更轻松地部署。

可能性是无穷的。AI 世界正以惊人的速度发展;借助 MCP,你不仅能使用 AI,更能与之共建、扩展它,并把它连接到真实世界,形成切实可用的能力。

感谢你与我一同走过这段旅程。现在,去创造一些了不起的东西吧!