Skip to content

Commit

Permalink
Merge branch 'v1.0' into micn/deep-linking
Browse files Browse the repository at this point in the history
* v1.0:
  feat: Implement a simplified reference agent and dev toolkit (#564)
  fix: sanitize server names for passing into model tool calls (#563)
  added a new mcp system + refactor to support it (#552)
  feat: port over nondeveloper system into mcp server (#561)
  fix: check server capability when client sends requests (#558)
  fix: update instructions (#557)
  • Loading branch information
michaelneale committed Jan 9, 2025
2 parents 68c3dbb + 32c7a3d commit 7d09e16
Show file tree
Hide file tree
Showing 25 changed files with 2,224 additions and 44 deletions.
12 changes: 8 additions & 4 deletions crates/goose-cli/src/commands/mcp.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
use anyhow::Result;
use goose_mcp::DeveloperRouter;
use goose_mcp::NonDeveloperRouter;
use goose_mcp::{Developer2Router, DeveloperRouter, JetBrainsRouter};
use mcp_server::router::RouterService;
use mcp_server::{ByteTransport, Server};
use mcp_server::{BoundedService, ByteTransport, Server};
use tokio::io::{stdin, stdout};

pub async fn run_server(name: &str) -> Result<()> {
tracing::info!("Starting MCP server");

let router = match name {
"developer" => Some(RouterService(DeveloperRouter::new())),
let router: Option<Box<dyn BoundedService>> = match name {
"developer" => Some(Box::new(RouterService(DeveloperRouter::new()))),
"developer2" => Some(Box::new(RouterService(Developer2Router::new()))),
"nondeveloper" => Some(Box::new(RouterService(NonDeveloperRouter::new()))),
"jetbrains" => Some(Box::new(RouterService(JetBrainsRouter::new()))),
_ => None,
};

Expand Down
4 changes: 3 additions & 1 deletion crates/goose-cli/src/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ pub async fn build_session<'a>(
// TODO use systems from the profile
// TODO once the client/server for MCP has stabilized, we should probably add InProcess transport to each
// and avoid spawning here. But it is at least included in the CLI for portability

let system = std::env::var("GOOSE_SYSTEM").unwrap_or("developer".to_string());
let config = SystemConfig::stdio(
std::env::current_exe()
.expect("should find the current executable")
.to_str()
.expect("should resolve executable to string path"),
)
.with_args(vec!["mcp", "developer"]);
.with_args(vec!["mcp", &system]);
agent
.add_system(config)
.await
Expand Down
7 changes: 6 additions & 1 deletion crates/goose-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ kill_tree = "0.2.4"
shellexpand = "3.1.0"
indoc = "2.0.5"
xcap = "0.0.14"
reqwest = { version = "0.11", features = ["json"] }
async-trait = "0.1"
parking_lot = "0.12"
chrono = { version = "0.4.38", features = ["serde"] }
dirs = "5.0.1"
tempfile = "3.8"

[dev-dependencies]
sysinfo = "0.32.1"
tempfile = "3.8"
1 change: 1 addition & 0 deletions crates/goose-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

```bash
npx @modelcontextprotocol/inspector cargo run -p developer
npx @modelcontextprotocol/inspector cargo run -p jetbrains
```

Then visit the Inspector in the browser window and test the different endpoints.
105 changes: 89 additions & 16 deletions crates/goose-mcp/src/developer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod process_store;

use anyhow::Result;
use base64::Engine;
use indoc::formatdoc;
use indoc::{formatdoc, indoc};
use serde_json::{json, Value};
use std::{
collections::HashMap,
Expand Down Expand Up @@ -52,39 +52,81 @@ impl Default for DeveloperRouter {
impl DeveloperRouter {
pub fn new() -> Self {
let bash_tool = Tool::new(
"bash".to_string(),
"Run a bash command in the shell in the current working directory".to_string(),
"bash",
indoc! {r#"
Run a bash command in the shell in the current working directory
- You can use multiline commands or && to execute multiple in one pass
- Directory changes **are not** persisted from one command to the next
- Sourcing files **is not** persisted from one command to the next
For example, you can use this style to execute python in a virtualenv
"source .venv/bin/active && python example1.py"
but need to repeat the source for subsequent commands in that virtualenv
"source .venv/bin/active && python example2.py"
"#},
json!({
"type": "object",
"required": ["command"],
"properties": {
"command": {"type": "string"}
"command": {
"type": "string",
"default": null,
"description": "The bash shell command to run."
},
}
}),
);

let text_editor_tool = Tool::new(
"text_editor".to_string(),
"Perform text editing operations on files.".to_string(),
"text_editor",
indoc! {r#"
Perform text editing operations on files.
The `command` parameter specifies the operation to perform. Allowed options are:
- `view`: View the content of a file.
- `write`: Write a file with the given content (create a new file or overwrite an existing).
- `str_replace`: Replace a string in a file with a new string.
- `undo_edit`: Undo the last edit made to a file.
"#},
json!({
"type": "object",
"required": ["command", "path"],
"properties": {
"path": {"type": "string"},
"path": {
"type": "string",
"description": "Path to the file. Can be absolute or relative to the system CWD"
},
"command": {
"enum": ["view", "write", "str_replace", "undo_edit"],
"description": "The command to run."
},
"new_str": {
"type": "string",
"enum": ["view", "write", "str_replace", "undo_edit"]
"default": null,
"description": "Required for the `str_replace` command."
},
"old_str": {
"type": "string",
"default": null,
"description": "Required for the `str_replace` command."
},
"file_text": {
"type": "string",
"default": null,
"description": "Required for `write` command."
},
"new_str": {"type": "string"},
"old_str": {"type": "string"},
"file_text": {"type": "string"}
}
}),
);

let list_windows_tool = Tool::new(
"list_windows".to_string(),
"List all open windows".to_string(),
"list_windows",
indoc! {r#"
List all available window titles that can be used with screen_capture.
Returns a list of window titles that can be used with the window_title parameter
of the screen_capture tool.
"#},
json!({
"type": "object",
"required": [],
Expand All @@ -93,8 +135,15 @@ impl DeveloperRouter {
);

let screen_capture_tool = Tool::new(
"screen_capture".to_string(),
"Capture a screenshot of a specified display or window.\nYou can capture either:\n1. A full display (monitor) using the display parameter\n2. A specific window by its title using the window_title parameter\n\nOnly one of display or window_title should be specified.".to_string(),
"screen_capture",
indoc! {r#"
Capture a screenshot of a specified display or window.
You can capture either:
1. A full display (monitor) using the display parameter
2. A specific window by its title using the window_title parameter
Only one of display or window_title should be specified.
"#},
json!({
"type": "object",
"required": [],
Expand All @@ -113,7 +162,31 @@ impl DeveloperRouter {
}),
);

let instructions = "Developer instructions...".to_string(); // Reuse from original code
let instructions = formatdoc! {r#"
The developer system is loaded in the directory listed below.
You can use the shell tool to run any command that would work on the relevant operating system.
Use the shell tool as needed to locate files or interact with the project. Only files
that have been read or modified using the edit tools will show up in the active files list.
bash
- Prefer ripgrep - `rg` - when you need to locate content, it will respected ignored files for
efficiency. **Avoid find and ls -r**
- to locate files by name: `rg --files | rg example.py`
- to locate consent inside files: `rg 'class Example'`
- The operating system for these commands is {os}
text_edit
- Always use 'view' command first before any edit operations
- File edits are tracked and can be undone with 'undo'
- String replacements must match exactly once in the file
- Line numbers start at 1 for insert operations
The write mode will do a full overwrite of the existing file, while the str_replace mode will edit it
using a find and replace. Choose the mode which will make the edit as simple as possible to execute.
"#,
os=std::env::consts::OS,
};

let cwd = std::env::current_dir().unwrap();
let mut resources = HashMap::new();
Expand Down
34 changes: 34 additions & 0 deletions crates/goose-mcp/src/developer2/lang.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use std::path::Path;

/// Get the markdown language identifier for a file extension
pub fn get_language_identifier(path: &Path) -> &'static str {
match path.extension().and_then(|ext| ext.to_str()) {
Some("rs") => "rust",
Some("py") => "python",
Some("js") => "javascript",
Some("ts") => "typescript",
Some("json") => "json",
Some("toml") => "toml",
Some("yaml") | Some("yml") => "yaml",
Some("sh") => "bash",
Some("go") => "go",
Some("md") => "markdown",
Some("html") => "html",
Some("css") => "css",
Some("sql") => "sql",
Some("java") => "java",
Some("cpp") | Some("cc") | Some("cxx") => "cpp",
Some("c") => "c",
Some("h") | Some("hpp") => "cpp",
Some("rb") => "ruby",
Some("php") => "php",
Some("swift") => "swift",
Some("kt") | Some("kts") => "kotlin",
Some("scala") => "scala",
Some("r") => "r",
Some("m") => "matlab",
Some("pl") => "perl",
Some("dockerfile") => "dockerfile",
_ => "",
}
}
Loading

0 comments on commit 7d09e16

Please sign in to comment.