Goose MCP 扩展开发实战:用 Python、TypeScript 或 Kotlin 构建你的第一个 MCP Server
本文基于 goose 内置教程 build-mcp-extension.md 整理,讲清 goose 中 MCP 扩展(Extension)的定位与开发全流程:从项目脚手架、基础 Server 搭建,到 Resources 与 Tools 的实现,再到 goose session / goose run 下的测试、日志与排错。读完后你可以用三种官方 SDK 之一独立完成一个可被 goose 加载的 stdio 扩展,并掌握系统化的调试方法。
一、背景:什么是 MCP 扩展,这份教程在 goose 里如何交付
MCP(Model Context Protocol)扩展让 AI Agent 能够通过协议使用工具(Tools)、读取资源(Resources)以及其他高级能力。扩展不必一次性实现全部功能,可以先从最简单的 Tools 或 Resources 起步。
这份教程在 goose 中并非普通的文档文件,而是内置教程扩展的一部分。从源码结构看,TutorialServer 通过 include_dir! 在编译期把 tutorial/tutorials 目录下的 Markdown 文件(包括本教程与 first-game.md)打进二进制中:
// crates/goose-mcp/src/tutorial/mod.rs
static TUTORIALS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/tutorial/tutorials");
该 Server 对外暴露一个名为 load_tutorial 的 MCP 工具:按文件名(去掉 .md)查找教程并返回 Markdown 全文,找不到时返回 INTERNAL_ERROR。它的 get_info() 会把当前所有可用教程的名称与首行摘要写进 instructions,并在初始化时声明自身为 goose-tutorial。测试用例 test_load_tutorial_success 验证了加载成功时返回的内容带有 audience = [Assistant] 的注解,即教程内容默认面向助手而非直接展示给用户。
在 goose 的内置扩展注册表中,tutorial 与其他内置扩展并列注册:
// crates/goose-mcp/src/lib.rs
pub static BUILTIN_EXTENSIONS: Lazy<HashMap<&'static str, SpawnServerFn>> = Lazy::new(|| {
HashMap::from([
builtin!(autovisualiser, AutoVisualiserRouter),
builtin!(computercontroller, ComputerControllerServer),
builtin!(memory, MemoryServer),
builtin!(tutorial, TutorialServer),
])
});
因此启动会话时加上 --with-builtin tutorial 即可让 goose 主动向你推荐可用教程,需要时用 load_tutorial 拉取完整内容。这份 build-mcp-extension.md 本身即是写给 Agent 的“引导剧本”:它规定了 Agent 应如何一步步带着用户完成扩展开发,而不是让用户一次性消化全部实现细节。
二、初始准备:先拉取最新的 SDK 参考代码
教程给出的第一条硬性要求是:在动手前,先把所选 SDK(Python、TypeScript 或 Kotlin)的官方仓库拉到本地临时目录作为权威参考,避免凭记忆写代码导致 API 过期。流程是:
- 创建临时目录
/tmp/mcp-reference; - 若对应 SDK 目录(
python-sdk/typescript-sdk/kotlin-sdk,均为 modelcontextprotocol 组织下的官方仓库)已存在,进入目录执行git pull,否则执行git clone; cat该 SDK 的README.md获取最新用法概要;- 后续开发中,用 ripgrep 在
/tmp/mcp-reference内检索具体实现,确保“以当前实现为准”。
例如(Python SDK 场景):
mkdir -p /tmp/mcp-reference && cd /tmp/mcp-reference
([ -d python-sdk/.git ] && (cd python-sdk && git pull) \
|| git clone https://github.com/modelcontextprotocol/python-sdk.git)
cat /tmp/mcp-reference/python-sdk/README.md
教程强调:凡是遇到编译或类型错误,修复前都应先回到参考 SDK 中查证真实实现,而不是自行假设 API 形态。
三、第 0 步:项目脚手架(Scaffolding)
教程要求先帮用户搭好项目目录与构建工具。三种 SDK 的初始化方式如下:
Python
- 用
uv init $PROJECT_NAME初始化项目; - 所有依赖管理统一使用
uv add,保持pyproject.toml最新; - 引入官方 SDK 包:
uv add mcp。
TypeScript
- 用
npm init -y初始化; - 引入官方 SDK 包:
@modelcontextprotocol/sdk(工具参数校验还需zod)。
Kotlin
- 教程给出了完整的
gradle init命令:
gradle init \
--type kotlin-application \
--dsl kotlin \
--test-framework junit-jupiter \
--package my.project \
--project-name $PROJECT_NAME \
--no-split-project \
--java-version 21
- 在
build.gradle.kts中加入依赖:"io.modelcontextprotocol:kotlin-sdk:0.3.0"; - 教程特别建议:克隆 Kotlin SDK 仓库后查看其
samples/kotlin-mcp-server示例目录,参考其中 Gradle 构建文件、属性、settings 与初始依赖配置,并以其中的Main.kt为基础实现进行扩展。
一个跨语言的通用提醒:脚手架阶段就要“总是对照参考 SDK 检查类型与正确用法”,这一点贯穿后续所有步骤。
四、第 1 步:基础 Server 搭建
初始 Server 文件的核心是:创建服务实例并绑定 stdio 传输。三种 SDK 的起始模式:
Python(FastMCP)
from mcp.server.fastmcp import FastMCP
from mcp.server.stdio import stdio_server
mcp = FastMCP("Extension Name")
if __name__ == "__main__":
mcp.run()
FastMCP 负责以装饰器方式注册 resources/tools 并处理协议细节;mcp.run() 默认监听 stdio,这正是 goose 扩展的默认通信方式。
TypeScript(McpServer + StdioServerTransport)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "Extension Name",
version: "1.0.0",
});
const transport = new StdioServerTransport();
await server.connect(transport);
Kotlin(Server + StdioServerTransport)
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
val server = Server(
serverInfo = Implementation(
name = "Extension Name",
version = "1.0.0"
)
)
val transport = StdioServerTransport()
server.connect(transport)
注意三者都实现了同一个心智模型:Server 实例承载能力注册(tools/resources),Transport 决定通信通道(本教程为 stdio,即由 goose 以子进程方式拉起并读写其标准输入输出)。
五、第 2 步:实现 Resources(向 LLM 提供数据)
Resources 用于向 LLM 暴露只读数据,通常以 URI 模板形式声明,其中 {param} 为路径参数占位符。
Python
@mcp.resource("example://{param}")
def get_example(param: str) -> str:
return f"Data for {param}"
TypeScript
server.resource(
"example",
new ResourceTemplate("example://{param}", { list: string}"),
async (uri, { param }) => ({
contents: [
{
uri: uri.href,
text: `Data for ${param}`,
},
],
}),
);
说明:ResourceTemplate 接收带 {param} 占位符的 URI 模板;回调的第二个参数是解析出的模板变量。教程原文中 list: undefined 表示不实现该资源的列表端点。
Kotlin
server.addResource(
uri = "example://{param}",
name = "Example",
description = "Example resource"
) { request ->
ReadResourceResult(
contents = listOf(
TextResourceContents(
text = "Data for ${request.params["param"]}",
uri = request.uri,
mimeType = "text/plain"
)
)
)
}
Kotlin 版本通过 ReadResourceResult 包装 TextResourceContents,并显式声明 mimeType。
六、第 3 步:实现 Tools(让 LLM 执行动作)
Tools 与 Resources 相反,是 LLM 可“调用”的动作入口。实现时的关键:给工具起清晰的名字与描述(描述会进入模型上下文,直接影响模型是否会选对工具)。
Python
@mcp.tool()
def example_tool(param: str) -> str:
"""Example description for tool"""
return f"Processed {param}"
函数 docstring 即工具描述,参数类型注解会被 SDK 自动转换为参数 Schema。
TypeScript
server.tool(
"example-tool",
"example description for tool",
{ param: z.string() },
async ({ param }) => ({
content: [{ type: "text", text: `Processed ${param}` }],
}),
);
第三个参数是 zod schema,用于声明并校验入参——这也是教程“Common Gotchas”中专门提醒的事项:TypeScript 里记得 import zod。
Kotlin
server.addTool(
name = "example-tool",
description = "Example tool"
) { request ->
ToolCallResult(
content = listOf(
TextContent(
type = "text",
text = "Processed ${request.arguments["param"]}"
)
)
)
}
教程建议的推进节奏是:先搭好基础 Server,然后“一次只加一个 resource 或 tool”,每加一个就测试一次,再进入下一个——不要把所有实现细节一次性倒出来。
七、测试与调试
7.1 首次测试:启动 goose 会话
教程明确:交互式会话无法由 Agent 代跑,需要用户在终端自行启动,并观察启动阶段是否有报错(若失败,把错误信息带回给 Agent 分析)。
# Python example
goose session --with-extension "python server.py"
# TypeScript example
goose session --with-extension "node server.js"
# Kotlin example
goose session --with-extension "java -jar build/libs/extension.jar"
对照 goose CLI 的扩展参数定义,--with-extension 接受完整启动命令(可重复指定多次),支持 [name:]ENV1=val1 ENV2=val2 command args... 格式:不带名称时扩展以启动器命令(python、node、uvx 等)命名,名称冲突时回退为整条命令行;同一组参数还支持 --with-streamable-http-extension(HTTP 端点)、--with-builtin(按名称加载内置扩展,如 tutorial)与 --no-profile(只使用命令行指定的扩展)。因此教程中的 --with-extension "python server.py" 实际上是让 goose 以子进程拉起你的 Server,通过 stdio 与其协商 MCP 协议。
7.2 无头(headless)反馈回路
教程同时指出:可以用无头模式跑自动化反馈回路,但进程若挂起会陷入卡死状态,需要先征得用户同意并提醒用户手动 kill 卡住的进程:
# Python example
goose run --with-extension "python server.py" --text "EXAMPLE PROMPT HERE"
# TypeScript example
goose run --with-extension "node server.js" --text "EXAMPLE PROMPT HERE"
# Kotlin example
goose run --with-extension "java -jar build/libs/extension.jar" --text "EXAMPLE PROMPT HERE"
--text 参数在 CLI 的输入选项 中定义,用于直接把提示文本交给 goose,替代交互式输入,适合脚本化验证。
7.3 功能验证:让 goose 真正用到你的扩展
会话成功启动后,验证方式很直接:
- 对 Tools:直接要求 goose 使用该工具;
- 对 Resources:要求 goose 读取相应数据。
教程给出的示例提示词:
"Please use the example-tool with parameter 'test'"
"Can you read the data from example://test-param"
若 goose 未正确选择你的工具,优先检查工具命名与描述是否具有区分度。
7.4 加日志:把黑盒变白盒
当报错信息不明确时,教程给出三种 SDK 的文件日志模式(统一写入 mcp_extension.log)。
Python:使用标准 logging 模块,basicConfig 指定 filename、level=logging.DEBUG 与格式串;在工具函数内记录入参、成功结果,异常时用 logging.error(..., exc_info=True) 保留堆栈:
import logging
logging.basicConfig(
filename='mcp_extension.log',
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
@mcp.tool()
def example_tool(param: str) -> str:
logging.debug(f"example_tool called with param: {param}")
try:
result = f"Processed {param}"
logging.debug(f"example_tool succeeded: {result}")
return result
except Exception as e:
logging.error(f"example_tool failed: {str(e)}", exc_info=True)
raise
TypeScript:用 fs.appendFileSync 自写追加式日志函数,在工具回调的 try/catch 中记录调用、成功与失败:
import * as fs from "fs";
function log(message: string) {
fs.appendFileSync(
"mcp_extension.log",
`${new Date().toISOString()} - ${message}\n`,
);
}
server.tool("example-tool", { param: z.string() }, async ({ param }) => {
log(`example-tool called with param: ${param}`);
try {
const result = `Processed ${param}`;
log(`example-tool succeeded: ${result}`);
return {
content: [{ type: "text", text: result }],
};
} catch (error) {
log(`example-tool failed: ${error}`);
throw error;
}
});
Kotlin:用 File.appendText 追加带时间戳的日志行,同样包裹工具主体:
import java.io.File
import java.time.LocalDateTime
fun log(message: String) {
File("mcp_extension.log").appendText("${LocalDateTime.now()} - $message\n")
}
server.addTool(
name = "example-tool",
description = "Example tool"
) { request ->
log("example-tool called with param: ${request.arguments["param"]}")
try {
val result = "Processed ${request.arguments["param"]}"
log("example-tool succeeded: $result")
ToolCallResult(
content = listOf(
TextContent(
type = "text",
text = result
)
)
)
} catch (e: Exception) {
log("example-tool failed: ${e.message}")
throw e
}
}
一个实践细节:stdio 扩展的标准输出被协议占用,绝不能 print/console.log 到 stdout,所以教程刻意选择“写文件”而不是控制台输出的日志方案。
7.5 排错流程
教程给出的排错顺序:
- 先看 goose 会话中是否有直接的错误消息;
- 报错不明确时:按上面的模式加日志 → 用更新后的代码重启会话 → 检查
mcp_extension.log; - 重点排查四类常见问题:参数类型错误或缺少参数、资源 URI 格式非法、工具实现内部异常、协议消息格式错误;
- 当用户把日志内容交给 Agent 分析时:找错误消息与堆栈、核对参数是否正确传递、验证实现是否符合 SDK 模式,并基于错误细节给出具体修复建议。
八、协作规范与常见陷阱
教程后半部分实际上是针对 Agent 的工作守则,同样适用于人类开发者自查:
- 先问清用户想构建什么,再确认使用哪种 SDK,然后才给具体实现;
- 始终先克隆参考 SDK 仓库、读
README.md,再用 ripgrep 检索具体示例——“参考真实实现,而不是做假设”; - 实现推进顺序:基础 Server → 逐个添加 resource/tool → 每步即测;
- 各语言的高频陷阱(教程原文 Common Gotchas):
- Python:确保装饰器正确导入(
FastMCP、stdio_server等); - TypeScript:记得导入
zod做参数校验; - Kotlin:注意正确的类型声明;
- Python:确保装饰器正确导入(
- 用户追问实现细节时:先查参考 SDK,再给出针对其 SDK 选择的具体指引;
- 角色定位是“引导与解释”:按用户节奏一步步推进,而非一次性倾倒全部代码。
九、小结
这条学习路径在 goose 仓库中有完整的支撑证据:教程正文 crates/goose-mcp/src/tutorial/tutorials/build-mcp-extension.md 定义方法论;TutorialServer 负责把教程按需注入会话;BUILTIN_EXTENSIONS 展示内置扩展的注册机制;ExtensionOptions 则解释了 --with-extension 如何把你的 Server 作为 stdio 子进程接入 goose。对仓库中已有实现的进一步参考,可以查看 mcp_replays 测试数据 与 goose-mcp 的 examples 示例。掌握“脚手架 → Server → Resources/Tools → 会话验证 → 文件日志排错”这条主线后,你就能用任意一种 SDK 为 goose 写出可运行的 MCP 扩展。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00