From d619c95273b1fdd7a38ea00110dc1734d63725f4 Mon Sep 17 00:00:00 2001 From: Tom Collier Date: Tue, 25 Mar 2025 12:28:18 -0700 Subject: [PATCH] feat(extensions): add Java/JDK support for MCP servers (#1816) --- documentation/docs/docker/Dockerfile | 8 ++ .../docs/getting-started/using-extensions.md | 70 +++++++++++++++ documentation/docs/tutorials/_template_.md | 4 + documentation/src/utils/install-links.ts | 2 +- ui/desktop/src/agent/utils.tsx | 1 + ui/desktop/src/bin/jbang | 89 +++++++++++++++++++ .../settings_v2/extensions/index.ts | 2 +- ui/desktop/src/extensions.tsx | 3 +- 8 files changed, 176 insertions(+), 3 deletions(-) create mode 100755 ui/desktop/src/bin/jbang diff --git a/documentation/docs/docker/Dockerfile b/documentation/docs/docker/Dockerfile index c76e4c75d6..ac72633df6 100644 --- a/documentation/docs/docker/Dockerfile +++ b/documentation/docs/docker/Dockerfile @@ -74,6 +74,8 @@ RUN apt-get update && apt-get install -y \ # Python python3 \ python3-pip \ + # Java + openjdk-17-jdk \ # Additional tools tree \ tmux \ @@ -82,6 +84,9 @@ RUN apt-get update && apt-get install -y \ # Install uv using curl RUN curl -LsSf https://astral.sh/uv/install.sh | sh +# Install JBang using curl +RUN curl -Ls https://sh.jbang.dev | bash -s - app setup + # Copy the built binaries COPY --from=builder /usr/src/goose/target/release/goose /usr/local/bin/ @@ -109,3 +114,6 @@ RUN git config --global init.defaultBranch main && \ # Add some helpful aliases RUN echo 'alias ll="ls -la"' >> ~/.bashrc && \ echo 'alias fd=fdfind' >> ~/.bashrc + +# Add JBang to PATH +RUN echo 'export PATH="$HOME/.jbang/bin:$PATH"' >> ~/.bashrc \ No newline at end of file diff --git a/documentation/docs/getting-started/using-extensions.md b/documentation/docs/getting-started/using-extensions.md index 808d633f11..32bd77638f 100644 --- a/documentation/docs/getting-started/using-extensions.md +++ b/documentation/docs/getting-started/using-extensions.md @@ -131,6 +131,8 @@ See available servers in the **[MCP Server Directory](https://www.pulsemcp.com/s #### Example of adding the [Knowledge Graph Memory MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/memory): + + ``` ┌ goose-configure │ @@ -155,6 +157,74 @@ See available servers in the **[MCP Server Directory](https://www.pulsemcp.com/s └ Added Knowledge Graph Memory extension ``` + + + + ``` + ┌ goose-configure + │ + ◇ What would you like to configure? + │ Add Extension + │ + ◇ What type of extension would you like to add? + │ Command-line Extension + │ + ◇ What would you like to call this extension? + │ Wikipedia Reader + │ + ◇ What command should be run? + │ uvx mcp-wiki + │ + ◇ Please set the timeout for this tool (in secs): + │ 300 + │ + ◆ Would you like to add environment variables? + │ No + │ + └ Added Wikipedia Reader extension + ``` + + + + +Note: Java and Kotlin extensions are only support on Linux and macOS + + ``` + ┌ goose-configure + │ + ◇ What would you like to configure? + │ Add Extension + │ + ◇ What type of extension would you like to add? + │ Command-line Extension + │ + ◇ What would you like to call this extension? + │ Spring Data Explorer + │ + ◇ What command should be run? + │ jbang -Dspring.profiles.active=dev org.example:spring-data-mcp:1.0.0 + │ + ◇ Please set the timeout for this tool (in secs): + │ 300 + │ + ◆ Would you like to add environment variables? + │ Yes + │ + ◇ Environment variable name: + │ SPRING_DATASOURCE_URL + │ + ◇ Environment variable value: + │ jdbc:postgresql://localhost:5432/mydb + │ + ◇ Add another environment variable? + │ No + │ + └ Added Spring Data Explorer extension + ``` + + + + diff --git a/documentation/docs/tutorials/_template_.md b/documentation/docs/tutorials/_template_.md index 7364f52645..8a5f8021fa 100644 --- a/documentation/docs/tutorials/_template_.md +++ b/documentation/docs/tutorials/_template_.md @@ -36,6 +36,10 @@ Note that you'll need [Node.js](https://nodejs.org/) installed on your system to Note that you'll need [uv](https://docs.astral.sh/uv/#installation) installed on your system to run this command, as it uses `uvx`. ::: +:::info +Note that you'll need [JBang](https://www.jbang.dev/download) installed on your system to run this command, as it uses `jbang`. +::: + 1. Run the `configure` command: diff --git a/documentation/src/utils/install-links.ts b/documentation/src/utils/install-links.ts index 156fee79f6..98a47a2e8a 100644 --- a/documentation/src/utils/install-links.ts +++ b/documentation/src/utils/install-links.ts @@ -11,7 +11,7 @@ export function getGooseInstallLink(server: MCPServer): string { return `goose://extension?${queryParams}`; } const parts = server.command.split(" "); - const baseCmd = parts[0]; // npx or uvx + const baseCmd = parts[0]; // jbang, npx or uvx const args = parts.slice(1); // remaining arguments const queryParams = [ `cmd=${encodeURIComponent(baseCmd)}`, diff --git a/ui/desktop/src/agent/utils.tsx b/ui/desktop/src/agent/utils.tsx index a834126fb7..17f36a4f41 100644 --- a/ui/desktop/src/agent/utils.tsx +++ b/ui/desktop/src/agent/utils.tsx @@ -19,6 +19,7 @@ export async function initializeAgent(model: string, provider: string) { export async function replaceWithShims(cmd: string) { const binaryPathMap: Record = { goosed: await window.electron.getBinaryPath('goosed'), + jbang: await window.electron.getBinaryPath('jbang'), npx: await window.electron.getBinaryPath('npx'), uvx: await window.electron.getBinaryPath('uvx'), }; diff --git a/ui/desktop/src/bin/jbang b/ui/desktop/src/bin/jbang new file mode 100755 index 0000000000..8748b44db8 --- /dev/null +++ b/ui/desktop/src/bin/jbang @@ -0,0 +1,89 @@ +#!/bin/bash + +# Enable strict mode to exit on errors and unset variables +set -euo pipefail + +# Set log file +LOG_FILE="/tmp/mcp.log" + +# Clear the log file at the start +> "$LOG_FILE" + +# Function for logging +log() { + local MESSAGE="$1" + echo "$(date +'%Y-%m-%d %H:%M:%S') - $MESSAGE" | tee -a "$LOG_FILE" +} + +# Trap errors and log them before exiting +trap 'log "An error occurred. Exiting with status $?."' ERR + +log "Starting jbang setup script." + +# Ensure ~/.config/goose/mcp-hermit/bin exists +log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist." +mkdir -p ~/.config/goose/mcp-hermit/bin + +# Change to the ~/.config/goose/mcp-hermit directory +log "Changing to directory ~/.config/goose/mcp-hermit." +cd ~/.config/goose/mcp-hermit + +# Check if hermit binary exists and download if not +if [ ! -f ~/.config/goose/mcp-hermit/bin/hermit ]; then + log "Hermit binary not found. Downloading hermit binary." + curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \ + | gzip -dc > ~/.config/goose/mcp-hermit/bin/hermit && chmod +x ~/.config/goose/mcp-hermit/bin/hermit + log "Hermit binary downloaded and made executable." +else + log "Hermit binary already exists. Skipping download." +fi + +log "setting hermit cache to be local for MCP servers" +mkdir -p ~/.config/goose/mcp-hermit/cache +export HERMIT_STATE_DIR=~/.config/goose/mcp-hermit/cache + +# Update PATH +export PATH=~/.config/goose/mcp-hermit/bin:$PATH +log "Updated PATH to include ~/.config/goose/mcp-hermit/bin." + +# Initialize hermit +log "Initializing hermit." +hermit init >> "$LOG_FILE" + +# Install OpenJDK using hermit +log "Installing OpenJDK with hermit." +hermit install openjdk@17 >> "$LOG_FILE" + +# Download and install jbang if not present +if [ ! -f ~/.config/goose/mcp-hermit/bin/jbang ]; then + log "Downloading and installing jbang." + curl -Ls https://sh.jbang.dev | bash -s - app setup + cp ~/.jbang/bin/jbang ~/.config/goose/mcp-hermit/bin/ +fi + +# Verify installations +log "Verifying installation locations:" +log "hermit: $(which hermit)" +log "java: $(which java)" +log "jbang: $(which jbang)" + +# Check for custom registry settings +log "Checking for GOOSE_JBANG_REGISTRY environment variable for custom jbang registry setup..." +if [ -n "${GOOSE_JBANG_REGISTRY:-}" ] && curl -s --head --fail "$GOOSE_JBANG_REGISTRY" > /dev/null; then + log "Checking custom goose registry availability: $GOOSE_JBANG_REGISTRY" + log "$GOOSE_JBANG_REGISTRY is accessible. Setting it as JBANG_REPO." + export JBANG_REPO="$GOOSE_JBANG_REGISTRY" +else + log "GOOSE_JBANG_REGISTRY is not set or not accessible. Using default jbang repository." +fi + +# Trust all jbang scripts that a user might install. Without this, Jbang will attempt to +# prompt the user to trust each script. However, Goose does not surfact this modal and without +# user input, the addExtension method will timeout and fail. +jbang --quiet trust add * + +# Final step: Execute jbang with passed arguments, always including --fresh and --quiet +log "Executing 'jbang' command with arguments: $*" +jbang --fresh --quiet "$@" || log "Failed to execute 'jbang' with arguments: $*" + +log "jbang setup script completed successfully." \ No newline at end of file diff --git a/ui/desktop/src/components/settings_v2/extensions/index.ts b/ui/desktop/src/components/settings_v2/extensions/index.ts index 508afaac0d..ff33ffa6e2 100644 --- a/ui/desktop/src/components/settings_v2/extensions/index.ts +++ b/ui/desktop/src/components/settings_v2/extensions/index.ts @@ -245,7 +245,7 @@ export async function addExtensionFromDeepLink( } // Validate that the command is one of the allowed commands - const allowedCommands = ['npx', 'uvx', 'goosed']; + const allowedCommands = ['jbang', 'npx', 'uvx', 'goosed']; if (!allowedCommands.includes(cmd)) { handleError( `Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`, diff --git a/ui/desktop/src/extensions.tsx b/ui/desktop/src/extensions.tsx index 62680423e3..0c607ca4d7 100644 --- a/ui/desktop/src/extensions.tsx +++ b/ui/desktop/src/extensions.tsx @@ -239,6 +239,7 @@ export async function loadAndAddStoredExtensions() { export async function replaceWithShims(cmd: string) { const binaryPathMap: Record = { goosed: await window.electron.getBinaryPath('goosed'), + jbang: await window.electron.getBinaryPath('jbang'), npx: await window.electron.getBinaryPath('npx'), uvx: await window.electron.getBinaryPath('uvx'), }; @@ -299,7 +300,7 @@ export async function addExtensionFromDeepLink( } // Validate that the command is one of the allowed commands - const allowedCommands = ['npx', 'uvx', 'goosed']; + const allowedCommands = ['jbang', 'npx', 'uvx', 'goosed']; if (!allowedCommands.includes(cmd)) { handleError( `Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`,