MCP服务器与笑话API集成

该服务是一个MCP服务器,用于与笑话RESTful API交互,提供随机笑话、按关键词搜索笑话、添加和删除笑话等功能。适用于开发者学习MCP服务器集成和API交互。

  • by poojanV55
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 poojanv55/mcp-jokes

使用说明

欢迎来到多伦多机器学习峰会研讨会!

本研讨会设计为一个代码长的会话,您将在其中学习如何设置和运行与RESTful API交互的MCP服务器,以及如何将其与MCP服务器集成以与 代号鹅.

研讨会议程

研讨会将首先介绍MCP的一些背景知识,以及用于连接其他计算系统的一些机制,如API和MCP服务器的实际演示。

接下来,我们将让每个人下载并安装Goose Desktop应用程序,并通过OpenRouter用二维码注册一些LLM学分。这些学分应该足以让我们完成研讨会,并在研讨会结束后不久到期。

我们将通过让每个人都运行笑话API服务器来结束这一部分。 从那里开始,我们将把所有内容分成大约15分钟的片段,回顾到目前为止的工作,讨论下一步的工作,然后一起编写代码。在处理代码时,我们将指示Goose定期重启MCP服务器。

  1. 运行我们的Joke API服务器,运行一个简单的MCP服务器,并在Goose中激活它,让Goose验证MCP服务器是否工作
  2. 添加MCP控制以从我们的API服务器获得一个笑话
  3. 添加MCP控件,按关键字或主题搜索笑话,有和没有计数
  4. 添加MCP控件以添加和删除笑话

我重视你的反馈!

请在休息时间或研讨会结束时提供对研讨会的反馈。如果你喜欢它,请考虑在社交媒体上留言,并在Block标记我和我们的团队:

  • https://x.com/iandouglas736, https://x.com/blockopensource
  • https://bsky.app/profile/iandouglas736.com, https://bsky.app/profile/opensource.block.xyz
  • https://youtube.com/@安道格拉斯,https://youtube.com/@区块开源

您还可以在GitHub上找到我们所有的集体开源作品:

  • https://github.com/iandouglas, https://github.com/block

承认幽默是主观的

我总是听一些老掉牙的笑话和谜语,我的孩子们喜欢“爸爸笑话”。不过,我承认幽默是主观的,英语中的“文字游戏”可能无法很好地翻译成其他语言或文化。如果本次研讨会中提供的任何笑话感觉不合适,请提供反馈,我将尽最大努力找到更具包容性和适合每个人的替代方案。

你想要一个鹅钥匙扣吗?

我有一些数量有限的有趣的3D打印钥匙链可以送给本次研讨会的参与者。如果你想要一个,请在研讨会结束时来找我,我很乐意在最后供应时给你一个。

设计由 伊利亚91 如果你想制作自己的,可以在这里找到:https://makerworld.com/en/models/749098-flexi-funny-goose#profileId-682552

请注意,这些不是“官方”的Block商品,我只是一个3D打印的书呆子,想和大家分享这些:)


工作坊步骤

虽然研讨会是一种合作形式,但欢迎您按照自己的节奏工作。对于我们构建过程的每个步骤,这个git仓库也有几个分支。如果你觉得卡住或迷失了方向,你可以将你的工作保存在另一个git分支中,查看下一节的分支,你就可以重新开始运行了!

当我们工作时,我们可能会经常提示Goose“再次尝试启用扩展”。

重要提示:

每个人的学习风格都不一样,但手写代码的肌肉记忆将有助于你理解我们正在构建的内容。如果你愿意,你当然可以复制和粘贴代码,但你将在大约10分钟内完成研讨会:)

因此,我强烈建议你和我一起编写代码,因为我解释了正在发生的事情,这样你就可以更深入地理解为什么事情是这样运作的,从而构建自己的MCP服务器。

我确实在下面的工作中提供了“文档块”,欢迎您复制/粘贴以节省时间。Dockblock为Goose等MCP客户提供了重要信息,但您不需要自己输入。 :)

没有人落后!

如果你被困在台阶上,别担心!这是一个研讨会,我们都在这里一起学习。如果你发现自己被困在一个步骤上,你可以寻求帮助,或者你可以将你的工作保存在一个单独的git分支中,然后查看下一个分支,让你赶上下一步!

要保存您的工作:

$ git co -b my-new-branch
$ git add .
$ git commit -m"this is where i got stuck"

稍后,您可以随时检查您的分支与下一个检查点,看看哪里出了问题:

$ git co branch-01-getting-started
$ git diff my-new-branch

这将生成预期代码和您正在编写的代码的“diff”格式,以便您可以看到发生了什么。

第一步:入门

如果你被困在这里,相关的git分支会被调用 branch-01-getting-started.

在本节中,我们将启动并运行一个基本的MCP服务器,并确保Goose可以激活该扩展。

  1. 运行笑话API

    首先,我们需要确保笑话API服务器正在运行。如果您还没有,请打开一个单独的终端窗口(在研讨会期间保持打开状态),并在 jokes_api 目录:

    cd jokes_api
    uv run main.py
    

    (第一次运行此命令时,下载和安装所需的软件包需要一些时间)

    您可以通过打开终端并运行以下命令来确认笑话API正在运行:

    curl http://localhost:8000/status
    
  2. 让我们调整一下 manifest.json 文件 清单文件将帮助指导我们的AI代理如何运行我们的MCP服务器,因此我们需要确保清单指向您的MCP服务器的正确路径。这是Goose将运行以启动MCP服务器的文件。

    更改manifest.json文件中看起来像这样的行:

        "cwd": "/TODO/path/to/jokes_mcp"
    

    指向系统上正确的磁盘路径。

研讨会中的所有其他工作将仅在 main.py 文件之后。

  1. 配置MCP服务器以调用API的状态检查 在……里面 main.py,我们在API上有一些带有状态检查的启动代码 /status Goose可以使用该端点来验证我们的MCP服务器是否正在运行,以及MCP服务器是否可以连接到笑话API。

    核实一下 main.py 将通过设置主机名或IP地址以及端口号来连接到笑话API

    API_HOSTNAME = "localhost"
    API_PORT = 8000
    
  2. 创建一个MCP“资源”来调用我们的状态检查端点:

     @mcp.resource("resource://status")
     def get_mcp_status() -> Dict:
         try:
             response = requests.get(f"http://{API_HOSTNAME}:{API_PORT}/status", timeout=5)
             return response.json()
         except requests.RequestException as e:
             logger.error(f"Error checking MCP status: {e}")
             return {"error": str(e)}
    
  3. 在我们的资源中添加一个“docblock” FastMCP会将函数注释中的任何信息(称为“documentaiton块”或“docblock”)传输给MCP客户端,以向其提供资源或工具的用途、如何使用等的完整上下文。我们在这里提供的信息越多,AI代理就越能使用我们的MCP服务器。

     @mcp.resource("resource://status")
     def get_mcp_status() -> Dict:
         """
         Check if the MCP server is running and can access 
         our API.
    
             This endpoint verifies the status of the MCP
             server by making a simple request to a known 
             resource. If the server responds correctly, it 
             is considered "up" and will tell you how many 
             jokes it has available.
    
         Returns:
             Dict: A dictionary containing the status and 
             number of jokes available.
    
         Example:
             status = read_resource("resource://status")
             # {"status":"running","jokes_count":35}
         """
         try:
             ...
         except ...
    
  4. 让我们运行MCP服务器! 现在我们已经设置了MCP服务器,我们可以运行它。在运行笑话API的终端中,打开一个新的终端窗口并运行以下命令:

    uv run main.py
    

    (第一次运行此命令时,下载和安装所需的软件包需要一些时间)

    要停止MCP进程,您可以尝试 Ctrl-C 在您启动它的终端窗口中,或者您尝试 Ctrl+\.

  5. 添加Goose扩展名 在Goose中,单击“齿轮”图标或使用菜单进入设置。点击“高级设置”并向下滚动,直到看到一个显示“添加自定义扩展”的按钮。点击该按钮并填写以下信息:

    • 名字:笑话
    • 类型:STDIO
    • 描述:从RESTful API访问笑话
    • 命令: uv run /path/to/jokes_mcp/main.py
      • 注:如果 uv 不在您的PATH中,您可能需要使用到的完整路径 uv 命令,例如 /usr/local/bin/uv 或者安装在系统上的任何位置,例如 /path/to/uv run /path/to/jokes_mcp/main.py 单击“保存更改”,滚动到窗口的最顶部,然后单击“返回”链接。
  6. 让Goose启用扩展 在Goose主窗口中,让Goose“启用笑话扩展并告诉我状态检查的响应内容”。 我们应该看到这样的回应:

     Based on the status check, the jokes extension is:
     Status: running
     Number of jokes in the collection: 36
    

第二步:开玩笑

如果你被困在这里,相关的git分支会被调用 branch-02-get-a-joke.

好了,现在我们开始讲好东西了!在本节中,我们将向MCP服务器添加一个资源,使我们能够从笑话API中随机获得笑话。

本节中的所有工作都将在MCP服务器中完成 main.py 文件。

  1. 将以下代码添加到main.py:
     @mcp.resource("resource://joke")
     def get_random_joke() -> str:
         """
         Get a random joke from the collection.
         
         This endpoint fetches a completely random joke from the entire collection,
         regardless of topic or content. Each call will likely return a different joke.
         
         Returns:
             str: A random joke text
         
         Example:
             joke = read_resource("resource://joke")
             # "Why did the programmer quit his job? Because he didn't get arrays!"
         """
         response = requests.get(f"http://{API_HOSTNAME}:{API_PORT}/joke")
    
         if response.status_code == 200:
             joke_data = response.json()
             return joke_data["joke"]
         
         return "Failed to retrieve joke."
    
  2. 重新启动扩展并测试它 在Goose中,让它“重新启动笑话扩展程序,告诉我一个随机的笑话”。您应该看到这样的响应:
     Here's your random joke:
    
     I just broke up with my mathematician girlfriend. She was obsessed with an X.
    
    如果你没有看到笑话,告诉Goose“确保我能在你的回复中看到笑话输出”。

步骤3:按关键字或主题搜索笑话

如果你被困在这里,相关的git分支会被调用 branch-03-search-jokes.

本节中的所有工作都将在MCP服务器中完成 main.py 文件。

现在,我们将添加一些资源,以根据搜索词获取笑话。搜索查询将查看笑话中的单词以及笑话的“主题”。我们要添加的第二个资源将采用“count”参数来限制返回的笑话数量。

我们可以将此作为单个资源并采用可选参数,但更直接的做法是为此设置两个单独的资源,这样AI代理就不必猜测该做什么。

  1. 将此代码添加到 main.py:

     def _search_jokes(query: str, count: int) -> List[str]:
         """Helper function to handle the actual joke search request"""
         logger.info(f"Searching for {count} jokes matching: {query}")
         try:
             response = requests.get(
                 f"http://{API_HOSTNAME}:{API_PORT}/joke/search",
                 params={"q": query, "count": count},
                 timeout=5
             )
             if response.status_code == 200:
                 jokes_data = response.json()
                 return [joke["joke"] for joke in jokes_data]
             else:
                 logger.error(f"Search failed with status {response.status_code}")
                 return [f"Failed to retrieve jokes. Status: {response.status_code}"]
         except requests.RequestException as e:
             logger.error(f"Request failed while searching for jokes: {e}")
             return [f"Failed to retrieve jokes due to network error"]
    
     @mcp.resource("resource://jokes/search/{query}")
     def search_one_joke(query: str) -> List[str]:
         return _search_jokes(query, 1)
    
     @mcp.resource("resource://jokes/search/{query}/{count}")
     def search_multiple_jokes(query: str, count: int) -> List[str]:
         return _search_jokes(query, count)
    

    以下是这两种资源的摘要:

     def search_one_joke(query: str) -> List[str]:
         """
         Search for a single joke containing the query string.
         
         Parameters:
             query: Search term to find in joke text or topics
         
         Returns:
             List containing one matching joke text
         
         Example:
             # Get one joke about animals
             jokes = read_resource("resource://jokes/search/animals")
             # Returns: ["Why don't cats like online shopping? They prefer a cat-alog!"]
         """
         ...
    
     @mcp.resource("resource://jokes/search/{query}/{count}")
     def search_multiple_jokes(query: str, count: int) -> List[str]:
         """
         Search for multiple jokes containing the query string.
         
         Parameters:
             query: Search term to find in joke text or topics
             count: Number of jokes to return
         
         Returns:
             List of matching joke texts
         
         Example:
             # Get three jokes about animals
             jokes = read_resource("resource://jokes/search/animals/3")
             # Returns: ["joke1", "joke2", "joke3"]
         """
         ...
    
  2. 测试新资源: 在Goose中,让它重新启动扩展程序,然后“搜索一个关于动物的笑话”,再“搜索3个关于动物们的笑话”。你应该看到这样的回应:

    Here's an animal-related joke for you:
    
    How do cows stay up to date? They read the Moo-spaper.
    

    然后,请鹅讲3个关于动物的笑话。您应该看到这样的响应:

    Here are 3 animal-related jokes:
    
    1. How do cows stay up to date? They read the Moo-spaper.
    2. What do you call a beehive without an exit? Unbelievable.
    3. If I ever find the doctor who screwed up my limb replacement surgery, 
       I'll kill him with my bear hands.
    

步骤4:添加笑话,按ID获取,按ID删除

如果你被困在这里,相关的git分支会被调用 branch-04-final-steps.

本节中的所有工作都将在MCP服务器中完成 main.py 文件。

下一节将实现MCP“工具”来添加笑话(这将返回一个ID值)、按ID获取笑话和按ID删除笑话。

  1. 将此代码添加到 main.py:

    @mcp.resource("resource://joke/{joke_id}")
    def get_joke_by_id(joke_id: int) -> str:
        response = requests.get(f"http://{API_HOSTNAME}:{API_PORT}/joke/{joke_id}")
    
        if response.status_code == 200:
            joke_data = response.json()
            return joke_data["joke"]
        else:
            return f"Failed to retrieve joke {joke_id}"
    
    @mcp.tool("add_joke")
    def add_joke(joke_text: str, topics: list[str]) -> str:
        response = requests.post(
            f"http://{API_HOSTNAME}:{API_PORT}/joke",
            json={"joke": joke_text, "topics": topics}
        )
        
        if response.status_code == 201:
            joke_data = response.json()
            return f"Added joke with ID {joke_data['id']}"
        else:
            return f"Failed to add joke. Status: {response.status_code}"
    
    @mcp.tool("delete_joke")
    def delete_joke(joke_id: int) -> str:
        try:
            response = requests.delete(f"http://{API_HOSTNAME}:{API_PORT}/joke/{joke_id}")
    
            if response.status_code == 200:
                return f"Successfully deleted joke {joke_id}"
            elif response.status_code == 404:
                return f"Joke {joke_id} not found"
            else:
                return f"Failed to delete joke {joke_id}. Error: {response.text}"
                
        except requests.RequestException as e:
            logger.error(f"Network error while deleting joke {joke_id}: {e}")
            return f"Network error while trying to delete joke {joke_id}"
            return f"Failed to delete joke {joke_id}. Status: {response.status_code}"
    
    

    以下是文档块:

    def get_joke_by_id(joke_id: int) -> str:
        """
        Get a specific joke by its ID.
        
        Retrieves a joke with a specific ID. This is useful when you want to
        reference a particular joke or verify a joke was added successfully.
        
        Args:
            joke_id (int): The ID of the joke to retrieve
        
        Returns:
            str: The joke text if found, otherwise an error message
        
        Example:
            # Get joke with ID 42
            joke = read_resource("resource://joke/42")
        """
        ...
    
    def add_joke(joke_text: str, topics: list[str]) -> str:
        """
        Add a new joke to the collection.
        
        Creates a new joke in the collection with the provided text and topics.
        The joke will be assigned a unique ID which is returned in the response.
        
        Args:
            joke_text (str): The text of the joke
            topics (list[str]): List of topics/categories for the joke
        
        Returns:
            str: A confirmation message with the joke ID or error message
        
        Example:
            result = add_joke(
                joke_text="Why did the function go to therapy? It had too many complex issues.",
                topics=["programming", "therapy", "puns"]
            )
            # Returns: "Added joke with ID 43"
        """
        ...
    
    def delete_joke(joke_id: int) -> str:
        """
        Delete a joke from the collection by its ID.
        
        Permanently removes a joke from the collection. This action cannot be undone.
        
        Args:
            joke_id (int): The ID of the joke to delete
        
        Returns:
            str: A confirmation message or error message
        
        Example:
            result = delete_joke(42)
            # Returns: "Successfully deleted joke 42"
        """
    
  2. 测试工作 请Goose再次重置扩展并测试集成。示例提示包括:

    Add a new joke about the day of the week, and choose some topics to assign to the joke.
    
    Fetch joke number 14
    
    Delete joke number 22
    

完整的代码

完整的最终项目可以在名为的分支中找到 branch-05-complete.

此分支将包含MCP服务器的最终完成代码,包括我们在本次研讨会中共同构建的所有资源和工具。