feat: tutorial extension (#1169)

Co-authored-by: Kalvin Chau <kalvin@squareup.com>
This commit is contained in:
Bradley Axen
2025-02-13 10:54:06 -08:00
committed by GitHub
parent 3ef552d7bf
commit 911f9b2033
8 changed files with 776 additions and 0 deletions

View File

@@ -422,6 +422,11 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
"Memory", "Memory",
"Tools to save and retrieve durable memories", "Tools to save and retrieve durable memories",
) )
.item(
"tutorial",
"Tutorial",
"Access interactive tutorials and guides",
)
.item("jetbrains", "JetBrains", "Connect to jetbrains IDEs") .item("jetbrains", "JetBrains", "Connect to jetbrains IDEs")
.interact()? .interact()?
.to_string(); .to_string();

View File

@@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use goose_mcp::{ use goose_mcp::{
ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, JetBrainsRouter, MemoryRouter, ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, JetBrainsRouter, MemoryRouter,
TutorialRouter,
}; };
use mcp_server::router::RouterService; use mcp_server::router::RouterService;
use mcp_server::{BoundedService, ByteTransport, Server}; use mcp_server::{BoundedService, ByteTransport, Server};
@@ -21,6 +22,7 @@ pub async fn run_server(name: &str) -> Result<()> {
Some(Box::new(RouterService(router))) Some(Box::new(RouterService(router)))
} }
"memory" => Some(Box::new(RouterService(MemoryRouter::new()))), "memory" => Some(Box::new(RouterService(MemoryRouter::new()))),
"tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))),
_ => None, _ => None,
}; };

View File

@@ -137,6 +137,10 @@ impl DeveloperRouter {
If you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that If you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that
this tool does not run indefinitely. this tool does not run indefinitely.
**Important**: Each shell command runs in its own process. Things like directory changes or
sourcing files do not persist between tool calls. So you may need to repeat them each time by
stringing together commands, e.g. `cd example && ls` or `source env/bin/activate && pip install numpy`
**Important**: Use ripgrep - `rg` - when you need to locate a file or a code reference, other solutions **Important**: Use ripgrep - `rg` - when you need to locate a file or a code reference, other solutions
may show ignored or hidden files. For example *do not* use `find` or `ls -r` may show ignored or hidden files. For example *do not* use `find` or `ls -r`
- List files by name: `rg --files | rg <filename>` - List files by name: `rg --files | rg <filename>`

View File

@@ -12,9 +12,11 @@ mod developer;
mod google_drive; mod google_drive;
mod jetbrains; mod jetbrains;
mod memory; mod memory;
mod tutorial;
pub use computercontroller::ComputerControllerRouter; pub use computercontroller::ComputerControllerRouter;
pub use developer::DeveloperRouter; pub use developer::DeveloperRouter;
pub use google_drive::GoogleDriveRouter; pub use google_drive::GoogleDriveRouter;
pub use jetbrains::JetBrainsRouter; pub use jetbrains::JetBrainsRouter;
pub use memory::MemoryRouter; pub use memory::MemoryRouter;
pub use tutorial::TutorialRouter;

View File

@@ -0,0 +1,168 @@
use anyhow::Result;
use include_dir::{include_dir, Dir};
use indoc::formatdoc;
use serde_json::{json, Value};
use std::{future::Future, pin::Pin};
use mcp_core::{
handler::{ResourceError, ToolError},
protocol::ServerCapabilities,
resource::Resource,
role::Role,
tool::Tool,
};
use mcp_server::router::CapabilitiesBuilder;
use mcp_server::Router;
use mcp_core::content::Content;
static TUTORIALS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/tutorial/tutorials");
pub struct TutorialRouter {
tools: Vec<Tool>,
instructions: String,
}
impl Default for TutorialRouter {
fn default() -> Self {
Self::new()
}
}
impl TutorialRouter {
pub fn new() -> Self {
let load_tutorial = Tool::new(
"load_tutorial".to_string(),
"Load a specific tutorial by name. The tutorial will be returned as markdown content that provides step by step instructions.".to_string(),
json!({
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"description": "Name of the tutorial to load, e.g. 'getting-started' or 'developer-mcp'"
}
}
}),
);
// Get base instructions and available tutorials
let available_tutorials = Self::get_available_tutorials();
let instructions = formatdoc! {r#"
Because the tutorial extension is enabled, be aware that the user may be new to using Goose
or looking for help with specific features. Proactively offer relevant tutorials when appropriate.
Available tutorials:
{tutorials}
The specific content of the tutorial are available in by running load_tutorial.
To run through a tutorial, make sure to be interactive with the user. Don't run more than
a few related tool calls in a row. Make sure to prompt the user for understanding and participation.
**Important**: Make sure that you provide guidance or info *before* you run commands, as the command will
run immediately for the user. For example while running a game tutorial, let the user know what to expect
before you run a command to start the game itself.
"#,
tutorials=available_tutorials,
};
Self {
tools: vec![load_tutorial],
instructions,
}
}
fn get_available_tutorials() -> String {
let mut tutorials = String::new();
for file in TUTORIALS_DIR.files() {
// Use first line for additional context
let first_line = file
.contents_utf8()
.and_then(|s| s.lines().next().map(|line| line.to_string()))
.unwrap_or_else(String::new);
if let Some(name) = file.path().file_stem() {
tutorials.push_str(&format!("- {}: {}\n", name.to_string_lossy(), first_line));
}
}
tutorials
}
async fn load_tutorial(&self, name: &str) -> Result<String, ToolError> {
let file_name = format!("{}.md", name);
let file = TUTORIALS_DIR
.get_file(&file_name)
.ok_or(ToolError::ExecutionError(format!(
"Could not locate tutorial '{}'",
name
)))?;
Ok(String::from_utf8_lossy(file.contents()).into_owned())
}
}
impl Router for TutorialRouter {
fn name(&self) -> String {
"tutorial".to_string()
}
fn instructions(&self) -> String {
self.instructions.clone()
}
fn capabilities(&self) -> ServerCapabilities {
CapabilitiesBuilder::new().with_tools(false).build()
}
fn list_tools(&self) -> Vec<Tool> {
self.tools.clone()
}
fn call_tool(
&self,
tool_name: &str,
arguments: Value,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
let this = self.clone();
let tool_name = tool_name.to_string();
Box::pin(async move {
match tool_name.as_str() {
"load_tutorial" => {
let name = arguments
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'name' parameter".to_string())
})?;
let content = this.load_tutorial(name).await?;
Ok(vec![
Content::text(content).with_audience(vec![Role::Assistant])
])
}
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
}
})
}
fn list_resources(&self) -> Vec<Resource> {
Vec::new()
}
fn read_resource(
&self,
_uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>> {
Box::pin(async move { Ok("".to_string()) })
}
}
impl Clone for TutorialRouter {
fn clone(&self) -> Self {
Self {
tools: self.tools.clone(),
instructions: self.instructions.clone(),
}
}
}

View File

@@ -0,0 +1,415 @@
# Building an Extension with MCP (Model Context Protocol)
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.
## Your Role
- You will help users implement MCP extensions using their chosen SDK
- You should adapt your explanations based on the user's experience level and questions
- Always reference the SDK implementations for accurate, up-to-date details
## Initial Setup
**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:
```bash
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
## Core Implementation Guide
### 0. Scaffolding
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**:
- Always check the reference SDK for typing and correct usage
- Python: Initialize a project using `uv init $PROJECT NAME`
- Python: Use `uv add` for all python package management, to keep `pyproject.toml` up to date
- Typescript: Initialize a project using `npm init -y`
- Kotlin: Use the following `gradle init` command to initialize:
```bash
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:
1. `mcp` for python
2. `"io.modelcontextprotocol:kotlin-sdk:0.3.0"` for kotlin
3. `@modelcontextprotocol/sdk` for typescript
**Important 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, youll 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.
### 1. Basic Server Setup
Help the user create their initial server file. Here are some patterns to get started with:
Python:
```python
from mcp.server.fastmcp import FastMCP
from mcp.server.stdio import stdio_server
mcp = FastMCP("Extension Name")
if __name__ == "__main__":
mcp.run()
```
TypeScript:
```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:
```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)
```
### 2. Implementing Resources
Resources provide data to the LLM. Guide users through implementing resources based on these patterns:
Python:
```python
@mcp.resource("example://{param}")
def get_example(param: str) -> str:
return f"Data for {param}"
```
TypeScript:
```typescript
server.resource(
"example",
new ResourceTemplate("example://{param}", { list: undefined }),
async (uri, { param }) => ({
contents: [
{
uri: uri.href,
text: `Data for ${param}`,
},
],
}),
);
```
Kotlin:
```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"
)
)
)
}
```
### 3. Implementing Tools
Tools allow the LLM to take actions. Guide users through implementing tools based on these patterns:
Python:
```python
@mcp.tool()
def example_tool(param: str) -> str:
"""Example description for tool"""
return f"Processed {param}"
```
TypeScript:
```typescript
server.tool(
"example-tool",
"example description for tool",
{ param: z.string() },
async ({ param }) => ({
content: [{ type: "text", text: `Processed ${param}` }],
}),
);
```
Kotlin:
```kotlin
server.addTool(
name = "example-tool",
description = "Example tool"
) { request ->
ToolCallResult(
content = listOf(
TextContent(
type = "text",
text = "Processed ${request.arguments["param"]}"
)
)
)
}
```
## Testing and Debugging Guide
Help users test their MCP extension using these steps:
### 1. Initial Testing
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 setup the environment
```bash
# 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.
```bash
# 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"
```
### 2. Testing Tools and Resources
Once the session starts successfully, guide users to test their implementation:
- For tools, they should ask Goose to use the tool directly
- For resources, they should ask Goose to access the relevant data
Example prompts they can use:
```
"Please use the example-tool with parameter 'test'"
"Can you read the data from example://test-param"
```
### 3. Adding Logging for Debugging
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:
```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:
```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:
```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
}
}
```
### 4. Debugging Process
When users encounter issues:
1. First, check if there are any immediate error messages in the Goose session
2. If the error isn't clear, guide them to:
- Add logging to their implementation using the patterns above
- Restart their session with the updated code
- Check the mcp_extension.log file for detailed error information
3. Common issues to watch for:
- Incorrect parameter types or missing parameters
- Malformed resource URIs
- Exceptions in tool implementation
- Protocol message formatting errors
4. If users share log contents with you:
- Look for error messages and stack traces
- Check if parameters are being passed correctly
- Verify the implementation matches the SDK patterns
- Suggest specific fixes based on the error details
## Important Guidelines for You (the Agent)
1. Always start by asking the user what they want to build
2. Always ask the user which SDK they want to use before providing specific implementation details
3. Always use the reference implementations:
- Always clone the relevant SDK repo before starting with basic steup
- After cloning the relevant SDK, find and `cat` the `README.md` for context
- Use ripgrep to find specific examples within the reference
- Reference real implementations rather than making assumptions
4. When building the project, if any compliation or type issues occur, _always_ check the reference SDK before making a fix.
5. When helping with implementations:
- Start with the basic server setup
- Add one resource or tool at a time
- Test each addition before moving on
6. Common Gotchas to Watch For:
- Python: Ensure decorators are properly imported
- TypeScript: Remember to import zod for parameter validation
- Kotlin: Pay attention to proper type declarations
7. When users ask about implementation details:
- First check the reference SDK
- Use ripgrep to find relevant examples
- Provide context-specific guidance based on their SDK choice
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.

View File

@@ -0,0 +1,178 @@
# Building Your First Game
This tutorial provides a framework for guiding a user through building their first simple game. The default suggestion is a Flappy Bird clone using Python and Pygame, but you should adapt based on user preferences and experience.
## Initial Discussion
Start by understanding the user's context and preferences:
1. Ask about their programming experience:
- Are they completely new to programming?
- Do they have experience with specific languages?
- Have they done any game development before?
2. Discuss game preferences:
- Suggest simple starter games they could build:
* Flappy Bird (default) - focuses on physics and collision
* Snake - focuses on grid-based movement and growth mechanics
* Pong - focuses on two-player interaction and ball physics
* Breakout - focuses on collision and scoring mechanics
- Let them suggest alternatives if they have something specific in mind
- Help them understand the complexity of their choice and adjust if needed
3. Choose technology stack:
- Default suggestion: Python + Pygame (beginner-friendly, cross-platform)
- Alternative suggestions based on user experience:
* JavaScript + Canvas (web-based, good for sharing)
* Lua + LÖVE (lightweight, good for learning)
* C# + MonoGame (good for Windows users/Unity transition)
- Consider factors like:
* Installation complexity on their OS
* Learning curve
* Available learning resources
* Their future goals in programming
## Environment Setup
Guide them through setting up their development environment:
1. Version Control:
- Help them install and configure git
- Explain basic version control concepts if they're new
- Create initial repository
2. Programming Language:
- Walk through installation for their chosen language
- Verify installation (help troubleshoot if needed)
- Explain how to run code in their environment
3. Dependency Management:
- Explain why dependency isolation is important
- For Python: Guide through virtualenv setup:
```bash
python -m venv env
source env/bin/activate # or env\Scripts\activate on Windows
```
- Similar isolation for other languages:
* Node: package.json
* Rust: Cargo.toml
* etc.
4. Game Framework:
- Install and verify chosen framework
- Create minimal test program
- Ensure they can run it successfully
## Project Structure
Help them set up a maintainable project:
1. Discuss project organization:
- File structure
- Code organization
- Asset management (if needed)
2. Create initial files:
- Main game file
- Configuration (if needed)
- Asset directories (if needed)
3. Set up version control:
- .gitignore for their stack
- Initial commit
- Explain commit strategy
## Core Game Loop
Guide them through building the basic game structure:
1. Window Setup:
- Creating a game window
- Setting up the game loop
- Handling basic events (exit, restart)
2. Game State:
- Define core game objects
- Set up state management
- Create update/draw separation
## Game Mechanics
Break down implementation into manageable pieces:
1. Player Interaction:
- Input handling
- Basic movement
- Test and refine "feel"
2. Core Mechanics:
- Main game elements (varies by game type)
- Basic collision detection
- Score tracking
3. Progressive Enhancement:
- Additional features
- Polish and refinement
- Bug fixes
## Testing and Refinement
Help them improve their game:
1. Playability:
- Test core mechanics
- Adjust difficulty
- Refine controls
2. Code Quality:
- Identify repetitive code
- Suggest improvements
- Explain benefits
## Extensions and Learning
Suggest next steps based on their interests:
1. Possible Enhancements:
- Graphics improvements
- Sound effects
- Additional features
- Menu systems
2. Learning Opportunities:
- Code structure improvements
- Performance optimization
- Advanced features
- Related topics to explore
## Notes for Agent
- Adapt the pace based on user understanding
- Provide more detailed explanations when needed
- Suggest breaks at good stopping points
- Celebrate small victories and progress
- Be ready to troubleshoot common issues:
* Installation problems
* Framework-specific errors
* Game logic bugs
* Performance issues
Remember to:
- Check understanding frequently
- Provide context for new concepts
- Relate to user's existing knowledge
- Be patient with debugging
- Encourage experimentation
- Maintain a positive learning environment
Default Implementation:
- If user has no strong preferences, guide them through:
* Python + Pygame
* Flappy Bird clone
* virtualenv for dependency management
* git for version control
- This combination provides:
* Minimal setup complexity
* Quick visible progress
* Clear next steps
* Manageable scope

View File

@@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use goose_mcp::{ use goose_mcp::{
ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, JetBrainsRouter, MemoryRouter, ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, JetBrainsRouter, MemoryRouter,
TutorialRouter,
}; };
use mcp_server::router::RouterService; use mcp_server::router::RouterService;
use mcp_server::{BoundedService, ByteTransport, Server}; use mcp_server::{BoundedService, ByteTransport, Server};
@@ -20,6 +21,7 @@ pub async fn run(name: &str) -> Result<()> {
Some(Box::new(RouterService(router))) Some(Box::new(RouterService(router)))
} }
"memory" => Some(Box::new(RouterService(MemoryRouter::new()))), "memory" => Some(Box::new(RouterService(MemoryRouter::new()))),
"tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))),
_ => None, _ => None,
}; };