feat: add fastmcp install goose command (#3040)

This commit is contained in:
Jeremiah Lowin 2026-01-31 10:53:24 -05:00 committed by GitHub
commit 1bbbee37a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 787 additions and 26 deletions

View file

@ -254,6 +254,7 @@
"integrations/claude-desktop",
"integrations/cursor",
"integrations/gemini-cli",
"integrations/goose",
"integrations/mcp-json-configuration"
]
},
@ -307,6 +308,7 @@
"python-sdk/fastmcp-cli-install-claude_desktop",
"python-sdk/fastmcp-cli-install-cursor",
"python-sdk/fastmcp-cli-install-gemini_cli",
"python-sdk/fastmcp-cli-install-goose",
"python-sdk/fastmcp-cli-install-mcp_json",
"python-sdk/fastmcp-cli-install-shared"
]

View file

@ -5,7 +5,7 @@ description: Connect FastMCP servers to ChatGPT in Chat and Deep Research modes
icon: message-smile
---
ChatGPT supports MCP servers through remote HTTP connections in two modes: **Chat mode** for interactive conversations and **Deep Research mode** for comprehensive information retrieval.
[ChatGPT](https://chatgpt.com/) supports MCP servers through remote HTTP connections in two modes: **Chat mode** for interactive conversations and **Deep Research mode** for comprehensive information retrieval.
<Tip>
**Developer Mode Required for Chat Mode**: To use MCP servers in regular ChatGPT conversations, you must first enable Developer Mode in your ChatGPT settings. This feature is available for ChatGPT Pro, Team, Enterprise, and Edu users.

View file

@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
Claude Code supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.
[Claude Code](https://docs.anthropic.com/en/docs/claude-code) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.
## Requirements

View file

@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
Claude Desktop supports MCP servers through local STDIO connections and remote servers (beta), allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.
[Claude Desktop](https://www.claude.com/download) supports MCP servers through local STDIO connections and remote servers (beta), allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.
<Note>
Remote MCP server support is currently in beta and available for users on Claude Pro, Max, Team, and Enterprise plans (as of June 2025). Most users will still need to use local STDIO connections.

View file

@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
Cursor supports MCP servers through multiple transport methods including STDIO, SSE, and Streamable HTTP, allowing you to extend Cursor's AI assistant with custom tools, resources, and prompts from your FastMCP servers.
[Cursor](https://www.cursor.com/) supports MCP servers through multiple transport methods including STDIO, SSE, and Streamable HTTP, allowing you to extend Cursor's AI assistant with custom tools, resources, and prompts from your FastMCP servers.
## Requirements

View file

@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
Gemini CLI supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Gemini's capabilities with custom tools, resources, and prompts from your FastMCP servers.
[Gemini CLI](https://geminicli.com/) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Gemini's capabilities with custom tools, resources, and prompts from your FastMCP servers.
## Requirements

178
docs/integrations/goose.mdx Normal file
View file

@ -0,0 +1,178 @@
---
title: Goose 🤝 FastMCP
sidebarTitle: Goose
description: Install and use FastMCP servers in Goose
icon: message-smile
---
import { VersionBadge } from "/snippets/version-badge.mdx"
import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
[Goose](https://block.github.io/goose/) is an open-source AI agent from Block that supports MCP servers as extensions. FastMCP can install your server directly into Goose using its deeplink protocol — one command opens Goose with an install dialog ready to go.
## Requirements
This integration uses Goose's deeplink protocol to register your server as a STDIO extension running via `uvx`. You must have Goose installed on your system for the deeplink to open automatically.
For remote deployments, configure your FastMCP server with HTTP transport and add it to Goose directly using `goose configure` or the config file.
## Create a Server
The examples in this guide will use the following simple dice-rolling server, saved as `server.py`.
```python server.py
import random
from fastmcp import FastMCP
mcp = FastMCP(name="Dice Roller")
@mcp.tool
def roll_dice(n_dice: int) -> list[int]:
"""Roll `n_dice` 6-sided dice and return the results."""
return [random.randint(1, 6) for _ in range(n_dice)]
if __name__ == "__main__":
mcp.run()
```
## Install the Server
### FastMCP CLI
<VersionBadge version="3.0.0" />
The easiest way to install a FastMCP server in Goose is using the `fastmcp install goose` command. This generates a `goose://` deeplink and opens it, prompting Goose to install the server.
```bash
fastmcp install goose server.py
```
The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
```bash
# These are equivalent if your server object is named 'mcp'
fastmcp install goose server.py
fastmcp install goose server.py:mcp
# Use explicit object name if your server has a different name
fastmcp install goose server.py:my_custom_server
```
Under the hood, the generated command uses `uvx` to run your server in an isolated environment. Goose requires `uvx` rather than `uv run`, so the install produces a command like:
```bash
uvx --with pandas fastmcp run /path/to/server.py
```
#### Dependencies
Use the `--with` flag to specify additional packages your server needs:
```bash
fastmcp install goose server.py --with pandas --with requests
```
Alternatively, you can use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]
}
}
```
#### Python Version
Use `--python` to specify which Python version your server should use:
```bash
fastmcp install goose server.py --python 3.11
```
<Note>
The Goose install uses `uvx`, which does not support `--project`, `--with-requirements`, or `--with-editable`. If you need these options, use `fastmcp install mcp-json` to generate a full configuration and add it to Goose manually.
</Note>
#### Environment Variables
Goose's deeplink protocol does not support environment variables. If your server needs them (like API keys), you have two options:
1. **Configure after install**: Run `goose configure` and add environment variables to the extension.
2. **Manual config**: Use `fastmcp install mcp-json` to generate the full configuration, then add it to `~/.config/goose/config.yaml` with the `envs` field.
### Manual Configuration
For more control, you can manually edit Goose's configuration file at `~/.config/goose/config.yaml`:
```yaml
extensions:
dice-roller:
name: Dice Roller
cmd: uvx
args: [fastmcp, run, /path/to/server.py]
enabled: true
type: stdio
timeout: 300
```
#### Dependencies
When manually configuring, add packages using `--with` flags in the args:
```yaml
extensions:
dice-roller:
name: Dice Roller
cmd: uvx
args: [--with, pandas, --with, requests, fastmcp, run, /path/to/server.py]
enabled: true
type: stdio
timeout: 300
```
#### Environment Variables
Environment variables can be specified in the `envs` field:
```yaml
extensions:
weather-server:
name: Weather Server
cmd: uvx
args: [fastmcp, run, /path/to/weather_server.py]
enabled: true
envs:
API_KEY: your-api-key
DEBUG: "true"
type: stdio
timeout: 300
```
You can also use `goose configure` to add extensions interactively, which prompts for environment variables.
<Warning>
**`uvx` (from `uv`) must be installed and available in your system PATH**. Goose uses `uvx` to run Python-based extensions in isolated environments.
</Warning>
## Using the Server
Once your server is installed, you can start using your FastMCP server with Goose.
Try asking Goose something like:
> "Roll some dice for me"
Goose will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:
> 🎲 Here are your dice rolls: 4, 6, 4
>
> You rolled 3 dice with a total of 14!
Goose can now access all the tools, resources, and prompts you've defined in your FastMCP server.

View file

@ -301,6 +301,7 @@ Install a MCP server in MCP client applications. FastMCP currently supports the
- **Claude Desktop** - Installs via direct configuration file modification
- **Cursor** - Installs via deeplink that opens Cursor for user confirmation
- **Gemini CLI** - Installs via Gemini CLI's built-in MCP management system
- **Goose** - Installs via deeplink that opens Goose for user confirmation (uses `uvx`)
- **MCP JSON** - Generates standard MCP JSON configuration for manual use
- **Stdio** - Outputs the shell command to run a server over stdio transport
@ -309,6 +310,7 @@ fastmcp install claude-code server.py
fastmcp install claude-desktop server.py
fastmcp install cursor server.py
fastmcp install gemini-cli server.py
fastmcp install goose server.py
fastmcp install mcp-json server.py
fastmcp install stdio server.py
```
@ -384,6 +386,9 @@ fastmcp install cursor server.py --env API_KEY=secret --env DEBUG=true
# Install with environment file
fastmcp install cursor server.py --env-file .env
# Install in Goose (uses uvx deeplink)
fastmcp install goose server.py --with pandas
# Install with specific Python version
fastmcp install claude-desktop server.py --python 3.11

View file

@ -6,6 +6,7 @@ from .claude_code import claude_code_command
from .claude_desktop import claude_desktop_command
from .cursor import cursor_command
from .gemini_cli import gemini_cli_command
from .goose import goose_command
from .mcp_json import mcp_json_command
from .stdio import stdio_command
@ -20,5 +21,6 @@ install_app.command(claude_code_command, name="claude-code")
install_app.command(claude_desktop_command, name="claude-desktop")
install_app.command(cursor_command, name="cursor")
install_app.command(gemini_cli_command, name="gemini-cli")
install_app.command(goose_command, name="goose")
install_app.command(mcp_json_command, name="mcp-json")
install_app.command(stdio_command, name="stdio")

View file

@ -1,12 +1,10 @@
"""Cursor integration for FastMCP install using Cyclopts."""
import base64
import os
import subprocess
import sys
from pathlib import Path
from typing import Annotated
from urllib.parse import quote, urlparse
from urllib.parse import quote
import cyclopts
from rich import print
@ -15,6 +13,7 @@ from fastmcp.mcp_config import StdioMCPServer, update_config_file
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
from .shared import open_deeplink as _shared_open_deeplink
from .shared import process_common_args
logger = get_logger(__name__)
@ -46,7 +45,7 @@ def generate_cursor_deeplink(
def open_deeplink(deeplink: str) -> bool:
"""Attempt to open a deeplink URL using the system's default handler.
"""Attempt to open a Cursor deeplink URL using the system's default handler.
Args:
deeplink: The deeplink URL to open
@ -54,21 +53,7 @@ def open_deeplink(deeplink: str) -> bool:
Returns:
True if the command succeeded, False otherwise
"""
parsed = urlparse(deeplink)
if parsed.scheme != "cursor":
logger.warning(f"Invalid deeplink scheme: {parsed.scheme}")
return False
try:
if sys.platform == "darwin": # macOS
subprocess.run(["open", deeplink], check=True, capture_output=True)
elif sys.platform == "win32": # Windows
os.startfile(deeplink)
else: # Linux and others
subprocess.run(["xdg-open", deeplink], check=True, capture_output=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return False
return _shared_open_deeplink(deeplink, expected_scheme="cursor")
def install_cursor_workspace(

View file

@ -0,0 +1,209 @@
"""Goose integration for FastMCP install using Cyclopts."""
import re
import sys
from pathlib import Path
from typing import Annotated
from urllib.parse import quote
import cyclopts
from rich import print
from fastmcp.utilities.logging import get_logger
from .shared import open_deeplink, process_common_args
logger = get_logger(__name__)
def _slugify(name: str) -> str:
"""Convert a display name to a URL-safe identifier.
Lowercases, replaces non-alphanumeric runs with hyphens,
and strips leading/trailing hyphens.
"""
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
return slug or "fastmcp-server"
def generate_goose_deeplink(
name: str,
command: str,
args: list[str],
*,
description: str = "MCP server installed via FastMCP",
) -> str:
"""Generate a Goose deeplink for installing an MCP extension.
Args:
name: Human-readable display name for the extension.
command: The executable command (e.g. "uv").
args: Arguments to the command.
description: Short description shown in Goose.
Returns:
A goose://extension?... deeplink URL.
"""
extension_id = _slugify(name)
params: list[str] = [f"cmd={quote(command, safe='')}"]
for arg in args:
params.append(f"arg={quote(arg, safe='')}")
params.append(f"id={quote(extension_id, safe='')}")
params.append(f"name={quote(name, safe='')}")
params.append(f"description={quote(description, safe='')}")
return f"goose://extension?{'&'.join(params)}"
def _build_uvx_command(
server_spec: str,
*,
python_version: str | None = None,
with_packages: list[str] | None = None,
) -> list[str]:
"""Build a uvx command for running a FastMCP server.
Goose requires uvx (not uv run) as the command. The uvx format is:
uvx [--with pkg] [--python X] fastmcp run <spec>
uvx automatically infers that the `fastmcp` command comes from the
`fastmcp` package, so --from is not needed.
"""
args: list[str] = ["uvx"]
if python_version:
args.extend(["--python", python_version])
for pkg in sorted(set(with_packages or [])):
if pkg != "fastmcp":
args.extend(["--with", pkg])
args.extend(["fastmcp", "run", server_spec])
return args
def install_goose(
file: Path,
server_object: str | None,
name: str,
*,
with_packages: list[str] | None = None,
python_version: str | None = None,
) -> bool:
"""Install FastMCP server in Goose via deeplink.
Args:
file: Path to the server file.
server_object: Optional server object name (for :object suffix).
name: Name for the extension in Goose.
with_packages: Optional list of additional packages to install.
python_version: Optional Python version to use.
Returns:
True if installation was successful, False otherwise.
"""
if server_object:
server_spec = f"{file.resolve()}:{server_object}"
else:
server_spec = str(file.resolve())
full_command = _build_uvx_command(
server_spec,
python_version=python_version,
with_packages=with_packages,
)
deeplink = generate_goose_deeplink(
name=name,
command=full_command[0],
args=full_command[1:],
)
print(f"[blue]Opening Goose to install '{name}'[/blue]")
if open_deeplink(deeplink, expected_scheme="goose"):
print("[green]Goose should now open with the installation dialog[/green]")
return True
else:
print(
"[red]Could not open Goose automatically.[/red]\n"
f"[blue]Please copy this link and open it in Goose: {deeplink}[/blue]"
)
return False
async def goose_command(
server_spec: str,
*,
server_name: Annotated[
str | None,
cyclopts.Parameter(
name=["--name", "-n"],
help="Custom name for the extension in Goose",
),
] = None,
with_packages: Annotated[
list[str] | None,
cyclopts.Parameter(
"--with",
help="Additional packages to install (can be used multiple times)",
),
] = None,
env_vars: Annotated[
list[str] | None,
cyclopts.Parameter(
"--env",
help="Environment variables in KEY=VALUE format (can be used multiple times)",
),
] = None,
env_file: Annotated[
Path | None,
cyclopts.Parameter(
"--env-file",
help="Load environment variables from .env file",
),
] = None,
python: Annotated[
str | None,
cyclopts.Parameter(
"--python",
help="Python version to use (e.g., 3.10, 3.11)",
),
] = None,
) -> None:
"""Install an MCP server in Goose.
Uses uvx to run the server. Environment variables are not included
in the deeplink; use `fastmcp install mcp-json` to generate a full
config for manual installation.
Args:
server_spec: Python file to install, optionally with :object suffix
"""
with_packages = with_packages or []
env_vars = env_vars or []
if env_vars or env_file:
print(
"[red]Goose deeplinks cannot include environment variables.[/red]\n"
"[yellow]Use `fastmcp install mcp-json` to generate a config, then add it "
"to your Goose config file with env vars: "
"https://block.github.io/goose/docs/getting-started/using-extensions/#config-entry[/yellow]"
)
sys.exit(1)
file, server_object, name, with_packages, _env_dict = await process_common_args(
server_spec, server_name, with_packages, env_vars, env_file
)
success = install_goose(
file=file,
server_object=server_object,
name=name,
with_packages=with_packages,
python_version=python,
)
if not success:
sys.exit(1)

View file

@ -1,8 +1,11 @@
"""Shared utilities for install commands."""
import json
import os
import subprocess
import sys
from pathlib import Path
from urllib.parse import urlparse
from dotenv import dotenv_values
from pydantic import ValidationError
@ -140,3 +143,32 @@ async def process_common_args(
env_dict[key] = value
return file, server_object, name, with_packages, env_dict
def open_deeplink(url: str, *, expected_scheme: str) -> bool:
"""Attempt to open a deeplink URL using the system's default handler.
Args:
url: The deeplink URL to open.
expected_scheme: The URL scheme to validate (e.g. "cursor", "goose").
Returns:
True if the command succeeded, False otherwise.
"""
parsed = urlparse(url)
if parsed.scheme != expected_scheme:
logger.warning(
f"Invalid deeplink scheme: {parsed.scheme}, expected {expected_scheme}"
)
return False
try:
if sys.platform == "darwin":
subprocess.run(["open", url], check=True, capture_output=True)
elif sys.platform == "win32":
os.startfile(url)
else:
subprocess.run(["xdg-open", url], check=True, capture_output=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return False

View file

@ -181,7 +181,7 @@ class TestOpenDeeplink:
"""Test opening deeplink on Windows."""
with patch("sys.platform", "win32"):
with patch(
"fastmcp.cli.install.cursor.os.startfile", create=True
"fastmcp.cli.install.shared.os.startfile", create=True
) as mock_startfile:
result = open_deeplink("cursor://test")
@ -251,7 +251,7 @@ class TestOpenDeeplink:
"""Test handling of OSError on Windows."""
with patch("sys.platform", "win32"):
with patch(
"fastmcp.cli.install.cursor.os.startfile", create=True
"fastmcp.cli.install.shared.os.startfile", create=True
) as mock_startfile:
mock_startfile.side_effect = OSError("File not found")
result = open_deeplink("cursor://test")

298
tests/cli/test_goose.py Normal file
View file

@ -0,0 +1,298 @@
from pathlib import Path
from unittest.mock import patch
from urllib.parse import parse_qs, unquote, urlparse
import pytest
from fastmcp.cli.install.goose import (
_build_uvx_command,
_slugify,
generate_goose_deeplink,
goose_command,
install_goose,
)
class TestSlugify:
def test_simple_name(self):
assert _slugify("My Server") == "my-server"
def test_special_characters(self):
assert _slugify("my_server (v2.0)") == "my-server-v2-0"
def test_already_slugified(self):
assert _slugify("my-server") == "my-server"
def test_empty_string(self):
assert _slugify("") == "fastmcp-server"
def test_only_special_chars(self):
assert _slugify("!!!") == "fastmcp-server"
def test_consecutive_hyphens_collapsed(self):
assert _slugify("a---b") == "a-b"
def test_leading_trailing_stripped(self):
assert _slugify("--hello--") == "hello"
class TestBuildUvxCommand:
def test_basic(self):
cmd = _build_uvx_command("server.py")
assert cmd == ["uvx", "fastmcp", "run", "server.py"]
def test_with_python_version(self):
cmd = _build_uvx_command("server.py", python_version="3.11")
assert cmd == [
"uvx",
"--python",
"3.11",
"fastmcp",
"run",
"server.py",
]
def test_with_packages(self):
cmd = _build_uvx_command("server.py", with_packages=["numpy", "pandas"])
assert "--with" in cmd
assert "numpy" in cmd
assert "pandas" in cmd
def test_fastmcp_not_in_with(self):
cmd = _build_uvx_command("server.py", with_packages=["fastmcp", "numpy"])
# fastmcp is the command itself, so it shouldn't appear in --with
with_indices = [i for i, v in enumerate(cmd) if v == "--with"]
with_values = [cmd[i + 1] for i in with_indices]
assert "fastmcp" not in with_values
def test_packages_sorted_and_deduplicated(self):
cmd = _build_uvx_command(
"server.py", with_packages=["pandas", "numpy", "pandas"]
)
with_indices = [i for i, v in enumerate(cmd) if v == "--with"]
with_values = [cmd[i + 1] for i in with_indices]
assert with_values == ["numpy", "pandas"]
def test_server_spec_with_object(self):
cmd = _build_uvx_command("server.py:app")
assert cmd[-1] == "server.py:app"
class TestGooseDeeplinkGeneration:
def test_basic_deeplink(self):
deeplink = generate_goose_deeplink(
name="test-server",
command="uvx",
args=["fastmcp", "run", "server.py"],
)
assert deeplink.startswith("goose://extension?")
parsed = urlparse(deeplink)
params = parse_qs(parsed.query)
assert params["cmd"] == ["uvx"]
assert params["name"] == ["test-server"]
assert params["id"] == ["test-server"]
def test_special_characters_in_name(self):
deeplink = generate_goose_deeplink(
name="my server (test)",
command="uvx",
args=["fastmcp", "run", "server.py"],
)
assert "name=my%20server%20%28test%29" in deeplink
parsed = urlparse(deeplink)
params = parse_qs(parsed.query)
assert params["id"] == ["my-server-test"]
def test_url_injection_protection(self):
deeplink = generate_goose_deeplink(
name="test&evil=true",
command="uvx",
args=["fastmcp", "run", "server.py"],
)
assert "name=test%26evil%3Dtrue" in deeplink
parsed = urlparse(deeplink)
params = parse_qs(parsed.query)
assert params["name"] == ["test&evil=true"]
def test_dangerous_characters_encoded(self):
dangerous_names = [
("test|calc", "test%7Ccalc"),
("test;calc", "test%3Bcalc"),
("test<calc", "test%3Ccalc"),
("test>calc", "test%3Ecalc"),
("test`calc", "test%60calc"),
("test$calc", "test%24calc"),
("test'calc", "test%27calc"),
('test"calc', "test%22calc"),
("test calc", "test%20calc"),
("test#anchor", "test%23anchor"),
("test?query=val", "test%3Fquery%3Dval"),
]
for dangerous_name, expected_encoded in dangerous_names:
deeplink = generate_goose_deeplink(
name=dangerous_name, command="uvx", args=["fastmcp", "run", "server.py"]
)
assert f"name={expected_encoded}" in deeplink, (
f"Failed to encode {dangerous_name}"
)
def test_custom_description(self):
deeplink = generate_goose_deeplink(
name="my-server",
command="uvx",
args=["fastmcp", "run", "server.py"],
description="My custom MCP server",
)
parsed = urlparse(deeplink)
params = parse_qs(parsed.query)
assert params["description"] == ["My custom MCP server"]
def test_args_with_special_characters(self):
deeplink = generate_goose_deeplink(
name="test",
command="uvx",
args=[
"--with",
"numpy>=1.20",
"fastmcp",
"run",
"server.py:MyApp",
],
)
parsed = urlparse(deeplink)
params = parse_qs(parsed.query)
assert "numpy>=1.20" in params["arg"]
assert "server.py:MyApp" in params["arg"]
def test_empty_args(self):
deeplink = generate_goose_deeplink(name="simple", command="python", args=[])
parsed = urlparse(deeplink)
params = parse_qs(parsed.query)
assert "arg" not in params
assert params["cmd"] == ["python"]
def test_command_with_path(self):
deeplink = generate_goose_deeplink(
name="test",
command="/usr/local/bin/uvx",
args=["fastmcp", "run", "server.py"],
)
parsed = urlparse(deeplink)
params = parse_qs(parsed.query)
assert params["cmd"] == ["/usr/local/bin/uvx"]
class TestInstallGoose:
@patch("fastmcp.cli.install.goose.open_deeplink")
@patch("fastmcp.cli.install.goose.print")
def test_success(self, mock_print, mock_open):
mock_open.return_value = True
result = install_goose(
file=Path("/path/to/server.py"),
server_object=None,
name="test-server",
)
assert result is True
mock_open.assert_called_once()
call_url = mock_open.call_args[0][0]
assert call_url.startswith("goose://extension?")
assert mock_open.call_args[1] == {"expected_scheme": "goose"}
@patch("fastmcp.cli.install.goose.open_deeplink")
@patch("fastmcp.cli.install.goose.print")
def test_success_uses_uvx(self, mock_print, mock_open):
mock_open.return_value = True
install_goose(
file=Path("/path/to/server.py"),
server_object=None,
name="test-server",
)
call_url = mock_open.call_args[0][0]
parsed = urlparse(call_url)
params = parse_qs(parsed.query)
assert params["cmd"] == ["uvx"]
assert "fastmcp" in params["arg"]
@patch("fastmcp.cli.install.goose.open_deeplink")
@patch("fastmcp.cli.install.goose.print")
def test_failure(self, mock_print, mock_open):
mock_open.return_value = False
result = install_goose(
file=Path("/path/to/server.py"),
server_object=None,
name="test-server",
)
assert result is False
@patch("fastmcp.cli.install.goose.open_deeplink")
@patch("fastmcp.cli.install.goose.print")
def test_with_server_object(self, mock_print, mock_open):
mock_open.return_value = True
install_goose(
file=Path("/path/to/server.py"),
server_object="app",
name="test-server",
)
call_url = mock_open.call_args[0][0]
parsed = urlparse(call_url)
params = parse_qs(parsed.query)
args = params["arg"]
assert any("server.py:app" in unquote(a) for a in args)
@patch("fastmcp.cli.install.goose.open_deeplink")
@patch("fastmcp.cli.install.goose.print")
def test_with_packages(self, mock_print, mock_open):
mock_open.return_value = True
install_goose(
file=Path("/path/to/server.py"),
server_object=None,
name="test-server",
with_packages=["numpy", "pandas"],
)
call_url = mock_open.call_args[0][0]
parsed = urlparse(call_url)
params = parse_qs(parsed.query)
args = params["arg"]
assert "numpy" in args
assert "pandas" in args
@patch("fastmcp.cli.install.goose.open_deeplink")
@patch("fastmcp.cli.install.goose.print")
def test_fallback_message_on_failure(self, mock_print, mock_open):
mock_open.return_value = False
install_goose(
file=Path("/path/to/server.py"),
server_object=None,
name="test-server",
)
fallback_calls = [
call
for call in mock_print.call_args_list
if "copy this link" in str(call).lower() or "goose://" in str(call)
]
assert len(fallback_calls) > 0
class TestGooseCommand:
@patch("fastmcp.cli.install.goose.install_goose")
@patch("fastmcp.cli.install.goose.process_common_args")
async def test_basic(self, mock_process, mock_install):
mock_process.return_value = (Path("server.py"), None, "test-server", [], {})
mock_install.return_value = True
await goose_command("server.py")
mock_install.assert_called_once_with(
file=Path("server.py"),
server_object=None,
name="test-server",
with_packages=[],
python_version=None,
)
@patch("fastmcp.cli.install.goose.install_goose")
@patch("fastmcp.cli.install.goose.process_common_args")
async def test_failure_exits(self, mock_process, mock_install):
mock_process.return_value = (Path("server.py"), None, "test-server", [], {})
mock_install.return_value = False
with pytest.raises(SystemExit) as exc_info:
await goose_command("server.py")
assert exc_info.value.code == 1

View file

@ -26,6 +26,7 @@ class TestInstallApp:
install_app.parse_args(["claude-desktop", "--help"])
install_app.parse_args(["cursor", "--help"])
install_app.parse_args(["gemini-cli", "--help"])
install_app.parse_args(["goose", "--help"])
install_app.parse_args(["mcp-json", "--help"])
install_app.parse_args(["stdio", "--help"])
except SystemExit:
@ -165,6 +166,53 @@ class TestCursorInstall:
assert bound.arguments["server_name"] == "test-server"
class TestGooseInstall:
"""Test goose install command."""
def test_goose_basic(self):
"""Test basic goose install command parsing."""
command, bound, _ = install_app.parse_args(
["goose", "server.py", "--name", "test-server"]
)
assert command is not None
assert bound.arguments["server_spec"] == "server.py"
assert bound.arguments["server_name"] == "test-server"
def test_goose_with_options(self):
"""Test goose install with various options."""
command, bound, _ = install_app.parse_args(
[
"goose",
"server.py",
"--name",
"test-server",
"--with",
"package1",
"--with",
"package2",
"--env",
"VAR1=value1",
]
)
assert bound.arguments["with_packages"] == ["package1", "package2"]
assert bound.arguments["env_vars"] == ["VAR1=value1"]
def test_goose_with_python(self):
"""Test goose install with --python option."""
command, bound, _ = install_app.parse_args(
[
"goose",
"server.py",
"--python",
"3.11",
]
)
assert bound.arguments["python"] == "3.11"
class TestMcpJsonInstall:
"""Test mcp-json install command."""
@ -323,6 +371,7 @@ class TestInstallCommandParsing:
["claude-desktop", "server.py"],
["cursor", "server.py"],
["gemini-cli", "server.py"],
["goose", "server.py"],
["stdio", "server.py"],
]
@ -351,6 +400,7 @@ class TestInstallCommandParsing:
["claude-desktop", "server.py", "--python", "3.11"],
["cursor", "server.py", "--python", "3.11"],
["gemini-cli", "server.py", "--python", "3.11"],
["goose", "server.py", "--python", "3.11"],
["mcp-json", "server.py", "--python", "3.11"],
["stdio", "server.py", "--python", "3.11"],
]