欢迎来到多伦多机器学习峰会研讨会!
本研讨会设计为一个代码长的会话,您将在其中学习如何设置和运行与RESTful API交互的MCP服务器,以及如何将其与MCP服务器集成以与 代号鹅.
研讨会议程
研讨会将首先介绍MCP的一些背景知识,以及用于连接其他计算系统的一些机制,如API和MCP服务器的实际演示。
接下来,我们将让每个人下载并安装Goose Desktop应用程序,并通过OpenRouter用二维码注册一些LLM学分。这些学分应该足以让我们完成研讨会,并在研讨会结束后不久到期。
我们将通过让每个人都运行笑话API服务器来结束这一部分。 从那里开始,我们将把所有内容分成大约15分钟的片段,回顾到目前为止的工作,讨论下一步的工作,然后一起编写代码。在处理代码时,我们将指示Goose定期重启MCP服务器。
- 运行我们的Joke API服务器,运行一个简单的MCP服务器,并在Goose中激活它,让Goose验证MCP服务器是否工作
- 添加MCP控制以从我们的API服务器获得一个笑话
- 添加MCP控件,按关键字或主题搜索笑话,有和没有计数
- 添加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可以激活该扩展。
- 运行笑话API
首先,我们需要确保笑话API服务器正在运行。如果您还没有,请打开一个单独的终端窗口(在研讨会期间保持打开状态),并在 jokes_api 目录:
cd jokes_api
uv run main.py(第一次运行此命令时,下载和安装所需的软件包需要一些时间)
您可以通过打开终端并运行以下命令来确认笑话API正在运行:
curl http://localhost:8000/status- 让我们调整一下
manifest.json文件
清单文件将帮助指导我们的AI代理如何运行我们的MCP服务器,因此我们需要确保清单指向您的MCP服务器的正确路径。这是Goose将运行以启动MCP服务器的文件。
更改manifest.json文件中看起来像这样的行:
"cwd": "/TODO/path/to/jokes_mcp"指向系统上正确的磁盘路径。
研讨会中的所有其他工作将仅在 main.py 文件之后。- 配置MCP服务器以调用API的状态检查
在……里面 main.py,我们在API上有一些带有状态检查的启动代码 /status Goose可以使用该端点来验证我们的MCP服务器是否正在运行,以及MCP服务器是否可以连接到笑话API。
核实一下 main.py 将通过设置主机名或IP地址以及端口号来连接到笑话API
API_HOSTNAME = "localhost"
API_PORT = 8000- 创建一个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)}- 在我们的资源中添加一个“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 ...- 让我们运行MCP服务器!
现在我们已经设置了MCP服务器,我们可以运行它。在运行笑话API的终端中,打开一个新的终端窗口并运行以下命令:
uv run main.py(第一次运行此命令时,下载和安装所需的软件包需要一些时间)
要停止MCP进程,您可以尝试 Ctrl-C 在您启动它的终端窗口中,或者您尝试 Ctrl+\.
- 添加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 单击“保存更改”,滚动到窗口的最顶部,然后单击“返回”链接。
- 让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 文件。
- 将以下代码添加到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."- 重新启动扩展并测试它
在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代理就不必猜测该做什么。
- 将此代码添加到
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"]
"""
...- 测试新资源:
在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删除笑话。
- 将此代码添加到
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"
"""- 测试工作
请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服务器的最终完成代码,包括我们在本次研讨会中共同构建的所有资源和工具。
