crates/goose-mcp/src/tutorial/tutorials/build-mcp-extension.md
For this tutorial you will guide the user through building an MCP extension. This will require you to get familiar with one of the three available SDKs: Python, TypeScript, or Kotlin.
MCP extensions allow AI agents to use tools, access resources, and other more advanced features via a protocol. The extension does not need to include all of these features.
Very Important: You (the agent) should always run the following so that you can get an up to date reference of the SDK to refer to.
Clone the SDK repo into a temp dir and if it already exists, cd into the folder
and run git pull, then and cat the README.md
Example:
mkdir -p /tmp/mcp-reference && cd /tmp/mcp-reference
([ -d [python|typescript|kotlin]-sdk/.git ] && (cd [python|typescript|kotlin]-sdk && git pull) \
|| git clone https://github.com/modelcontextprotocol/[python|typescript|kotlin]-sdk.git
cat /tmp/mcp-reference/[python|typescript|kotlin]-sdk/README.md
Then, as needed, use ripgrep to search within the mcp-reference dir. Important: reference this implementation to make sure you have up to date implementation
You should help the user scaffold out a project directory if they don't already have one. This includes any necessary build tools or dependencies.
Important:
uv init $PROJECT NAMEuv add for all python package management, to keep pyproject.toml up to datenpm init -ygradle init command to initialize:
gradle init \
--type kotlin-application \
--dsl kotlin \
--test-framework junit-jupiter \
--package my.project \
--project-name $PROJECT_NAME \
--no-split-project \
--java-version 21
Include the relevant SDK package:
mcp for python"io.modelcontextprotocol:kotlin-sdk:0.3.0" for kotlin@modelcontextprotocol/sdk for typescriptImportant for kotlin development: To get started with a Kotlin MCP server, look at the kotlin-mcp-server example included in the Kotlin SDK. After cloning the SDK repository, you can find this sample inside the samples/kotlin-mcp-server directory. There, you’ll see how the Gradle build files, properties, and settings are configured, as well as the initial set of dependencies. Use these existing gradle configurations to get the user started. Be sure to check out the Main.kt file for a basic implementation that you can build upon.
Help the user create their initial server file. Here are some patterns to get started with:
Python:
from mcp.server.fastmcp import FastMCP
from mcp.server.stdio import stdio_server
mcp = FastMCP("Extension Name")
if __name__ == "__main__":
mcp.run()
TypeScript:
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:
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)
Resources provide data to the LLM. Guide users through implementing resources based on these patterns:
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: undefined }),
async (uri, { param }) => ({
contents: [
{
uri: uri.href,
text: `Data for ${param}`,
},
],
}),
);
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"
)
)
)
}
Tools allow the LLM to take actions. Guide users through implementing tools based on these patterns:
Python:
@mcp.tool()
def example_tool(param: str) -> str:
"""Example description for tool"""
return f"Processed {param}"
TypeScript:
server.tool(
"example-tool",
"example description for tool",
{ param: z.string() },
async ({ param }) => ({
content: [{ type: "text", text: `Processed ${param}` }],
}),
);
Kotlin:
server.addTool(
name = "example-tool",
description = "Example tool"
) { request ->
ToolCallResult(
content = listOf(
TextContent(
type = "text",
text = "Processed ${request.arguments["param"]}"
)
)
)
}
Help users test their MCP extension using these steps:
Instruct users to start a goose session with their extension.
Important: You cannot start the goose session for them, as it is interactive. You will have to let them know to start it in a terminal. Make sure you include instructions on how to set up the environment
# 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"
Tell users to watch for startup errors. If the session fails to start, they should share the error message with you for debugging.
Note: You can run a feedback loop using a headless goose session, however if the process hangs you get into a stuck action. Ask the user if they want you to do that, and let them know they will manually need to kill any stuck processes.
# 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"
Once the session starts successfully, guide users to test their implementation:
Example prompts they can use:
"Please use the example-tool with parameter 'test'"
"Can you read the data from example://test-param"
If the user encounters an unclear error, guide them to add file-based logging to the server. Here are the patterns for each SDK:
Python:
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:
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:
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
}
}
When users encounter issues:
First, check if there are any immediate error messages in the goose session
If the error isn't clear, guide them to:
Common issues to watch for:
If users share log contents with you:
Always start by asking the user what they want to build
Always ask the user which SDK they want to use before providing specific implementation details
Always use the reference implementations:
cat the README.md for contextWhen building the project, if any compilation or type issues occur, always check the reference SDK before making a fix.
When helping with implementations:
Common Gotchas to Watch For:
When users ask about implementation details:
Remember: Your role is to guide and explain, adapting based on the user's needs and questions. Don't dump all implementation details at once - help users build their extension step by step.