From 911f9b20332badaa75aa29abb3a854f2d567da8f Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Thu, 13 Feb 2025 10:54:06 -0800 Subject: [PATCH] feat: tutorial extension (#1169) Co-authored-by: Kalvin Chau --- crates/goose-cli/src/commands/configure.rs | 5 + crates/goose-cli/src/commands/mcp.rs | 2 + crates/goose-mcp/src/developer/mod.rs | 4 + crates/goose-mcp/src/lib.rs | 2 + crates/goose-mcp/src/tutorial/mod.rs | 168 +++++++ .../tutorial/tutorials/build-mcp-extension.md | 415 ++++++++++++++++++ .../src/tutorial/tutorials/first-game.md | 178 ++++++++ crates/goose-server/src/commands/mcp.rs | 2 + 8 files changed, 776 insertions(+) create mode 100644 crates/goose-mcp/src/tutorial/mod.rs create mode 100644 crates/goose-mcp/src/tutorial/tutorials/build-mcp-extension.md create mode 100644 crates/goose-mcp/src/tutorial/tutorials/first-game.md diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index d6a48f8242..f0a63e496b 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -422,6 +422,11 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { "Memory", "Tools to save and retrieve durable memories", ) + .item( + "tutorial", + "Tutorial", + "Access interactive tutorials and guides", + ) .item("jetbrains", "JetBrains", "Connect to jetbrains IDEs") .interact()? .to_string(); diff --git a/crates/goose-cli/src/commands/mcp.rs b/crates/goose-cli/src/commands/mcp.rs index 12748112d7..805fe6e7f9 100644 --- a/crates/goose-cli/src/commands/mcp.rs +++ b/crates/goose-cli/src/commands/mcp.rs @@ -1,6 +1,7 @@ use anyhow::Result; use goose_mcp::{ ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, JetBrainsRouter, MemoryRouter, + TutorialRouter, }; use mcp_server::router::RouterService; use mcp_server::{BoundedService, ByteTransport, Server}; @@ -21,6 +22,7 @@ pub async fn run_server(name: &str) -> Result<()> { Some(Box::new(RouterService(router))) } "memory" => Some(Box::new(RouterService(MemoryRouter::new()))), + "tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))), _ => None, }; diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index 492f4b56af..ee326ddd4c 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -137,6 +137,10 @@ impl DeveloperRouter { If you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that 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 may show ignored or hidden files. For example *do not* use `find` or `ls -r` - List files by name: `rg --files | rg ` diff --git a/crates/goose-mcp/src/lib.rs b/crates/goose-mcp/src/lib.rs index f966cf55af..991159f810 100644 --- a/crates/goose-mcp/src/lib.rs +++ b/crates/goose-mcp/src/lib.rs @@ -12,9 +12,11 @@ mod developer; mod google_drive; mod jetbrains; mod memory; +mod tutorial; pub use computercontroller::ComputerControllerRouter; pub use developer::DeveloperRouter; pub use google_drive::GoogleDriveRouter; pub use jetbrains::JetBrainsRouter; pub use memory::MemoryRouter; +pub use tutorial::TutorialRouter; diff --git a/crates/goose-mcp/src/tutorial/mod.rs b/crates/goose-mcp/src/tutorial/mod.rs new file mode 100644 index 0000000000..9d6ba3d7c3 --- /dev/null +++ b/crates/goose-mcp/src/tutorial/mod.rs @@ -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, + 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 { + 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 { + self.tools.clone() + } + + fn call_tool( + &self, + tool_name: &str, + arguments: Value, + ) -> Pin, 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 { + Vec::new() + } + + fn read_resource( + &self, + _uri: &str, + ) -> Pin> + 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(), + } + } +} diff --git a/crates/goose-mcp/src/tutorial/tutorials/build-mcp-extension.md b/crates/goose-mcp/src/tutorial/tutorials/build-mcp-extension.md new file mode 100644 index 0000000000..33502add69 --- /dev/null +++ b/crates/goose-mcp/src/tutorial/tutorials/build-mcp-extension.md @@ -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, 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. + +### 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. diff --git a/crates/goose-mcp/src/tutorial/tutorials/first-game.md b/crates/goose-mcp/src/tutorial/tutorials/first-game.md new file mode 100644 index 0000000000..1c773f387d --- /dev/null +++ b/crates/goose-mcp/src/tutorial/tutorials/first-game.md @@ -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 \ No newline at end of file diff --git a/crates/goose-server/src/commands/mcp.rs b/crates/goose-server/src/commands/mcp.rs index 773c1f38a4..5a23339a76 100644 --- a/crates/goose-server/src/commands/mcp.rs +++ b/crates/goose-server/src/commands/mcp.rs @@ -1,6 +1,7 @@ use anyhow::Result; use goose_mcp::{ ComputerControllerRouter, DeveloperRouter, GoogleDriveRouter, JetBrainsRouter, MemoryRouter, + TutorialRouter, }; use mcp_server::router::RouterService; use mcp_server::{BoundedService, ByteTransport, Server}; @@ -20,6 +21,7 @@ pub async fn run(name: &str) -> Result<()> { Some(Box::new(RouterService(router))) } "memory" => Some(Box::new(RouterService(MemoryRouter::new()))), + "tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))), _ => None, };