From f9a698dcf935d07472466c5424d3c52da78f139a Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Sun, 6 Jul 2025 10:23:50 -0400
Subject: [PATCH] Add MCP json output from CLI (#1056)
---
docs/css/style.css | 1 +
docs/docs.json | 9 +-
docs/integrations/anthropic.mdx | 4 +-
docs/integrations/chatgpt.mdx | 2 +-
docs/integrations/claude-code.mdx | 9 +-
docs/integrations/claude-desktop.mdx | 4 +-
docs/integrations/cursor.mdx | 27 +-
docs/integrations/eunomia-authorization.mdx | 4 +-
docs/integrations/gemini.mdx | 4 +-
docs/integrations/mcp-json-configuration.mdx | 347 +++++++++++++++++++
docs/integrations/openai.mdx | 4 +-
docs/patterns/cli.mdx | 52 ++-
docs/{integrations => patterns}/contrib.mdx | 0
docs/patterns/tool-transformation.mdx | 1 -
docs/snippets/local-focus.mdx | 7 +
pyproject.toml | 1 +
src/fastmcp/cli/cli.py | 6 +-
src/fastmcp/cli/install/__init__.py | 28 +-
src/fastmcp/cli/install/claude_code.py | 75 ++++
src/fastmcp/cli/install/claude_desktop.py | 74 ++++
src/fastmcp/cli/install/cursor.py | 71 ++++
src/fastmcp/cli/install/install.py | 198 -----------
src/fastmcp/cli/install/mcp_config.py | 179 ++++++++++
src/fastmcp/cli/install/shared.py | 87 +++++
tests/cli/test_mcp_config.py | 199 +++++++++++
uv.lock | 2 +
26 files changed, 1163 insertions(+), 232 deletions(-)
create mode 100644 docs/integrations/mcp-json-configuration.mdx
rename docs/{integrations => patterns}/contrib.mdx (100%)
create mode 100644 docs/snippets/local-focus.mdx
delete mode 100644 src/fastmcp/cli/install/install.py
create mode 100644 src/fastmcp/cli/install/mcp_config.py
create mode 100644 src/fastmcp/cli/install/shared.py
create mode 100644 tests/cli/test_mcp_config.py
diff --git a/docs/css/style.css b/docs/css/style.css
index 9716917b1..6f526c7bc 100644
--- a/docs/css/style.css
+++ b/docs/css/style.css
@@ -1,6 +1,7 @@
/* Code highlighting -- target only inline code elements, not code blocks */
p code:not(pre code),
table code:not(pre code),
+.prose code:not(pre code),
li code:not(pre code),
h1 code:not(pre code),
h2 code:not(pre code),
diff --git a/docs/docs.json b/docs/docs.json
index 9cb7bf269..fc4ea0728 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -140,10 +140,10 @@
"integrations/claude-code",
"integrations/claude-desktop",
"integrations/cursor",
- "integrations/gemini",
- "integrations/openai",
"integrations/eunomia-authorization",
- "integrations/contrib"
+ "integrations/gemini",
+ "integrations/mcp-json-configuration",
+ "integrations/openai"
]
},
{
@@ -153,7 +153,8 @@
"patterns/decorating-methods",
"patterns/http-requests",
"patterns/testing",
- "patterns/cli"
+ "patterns/cli",
+ "patterns/contrib"
]
},
{
diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx
index 6c50aedc7..c54b46a64 100644
--- a/docs/integrations/anthropic.mdx
+++ b/docs/integrations/anthropic.mdx
@@ -1,8 +1,8 @@
---
-title: Anthropic API + FastMCP
+title: Anthropic API 🤝 FastMCP
sidebarTitle: Anthropic API
description: Call FastMCP servers from the Anthropic API
-icon: message-smile
+icon: message-code
---
import { VersionBadge } from "/snippets/version-badge.mdx"
diff --git a/docs/integrations/chatgpt.mdx b/docs/integrations/chatgpt.mdx
index 797ea439d..6b1c260df 100644
--- a/docs/integrations/chatgpt.mdx
+++ b/docs/integrations/chatgpt.mdx
@@ -1,5 +1,5 @@
---
-title: ChatGPT + FastMCP
+title: ChatGPT 🤝 FastMCP
sidebarTitle: ChatGPT
description: Connect FastMCP servers to ChatGPT Deep Research
icon: message-smile
diff --git a/docs/integrations/claude-code.mdx b/docs/integrations/claude-code.mdx
index 85aaf13cf..b17c63624 100644
--- a/docs/integrations/claude-code.mdx
+++ b/docs/integrations/claude-code.mdx
@@ -1,5 +1,5 @@
---
-title: Claude Code + FastMCP
+title: Claude Code 🤝 FastMCP
sidebarTitle: Claude Code
description: Install and use FastMCP servers in Claude Code
icon: message-smile
@@ -7,13 +7,12 @@ tag: NEW
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { LocalFocusTip } from "/snippets/local-focus.mdx"
+
+
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.
-
-This guide focuses specifically on installing local FastMCP server files directly into Claude Code using STDIO transport. For deploying remote servers using SSE or HTTP transports, see the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp).
-
-
## Requirements
This integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly using Claude Code's built-in MCP management commands.
diff --git a/docs/integrations/claude-desktop.mdx b/docs/integrations/claude-desktop.mdx
index a6ca49811..2d37d523e 100644
--- a/docs/integrations/claude-desktop.mdx
+++ b/docs/integrations/claude-desktop.mdx
@@ -1,12 +1,14 @@
---
-title: Claude Desktop + FastMCP
+title: Claude Desktop 🤝 FastMCP
sidebarTitle: Claude Desktop
description: Call FastMCP servers from Claude Desktop
icon: message-smile
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { LocalFocusTip } from "/snippets/local-focus.mdx"
+
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.
diff --git a/docs/integrations/cursor.mdx b/docs/integrations/cursor.mdx
index c52b7143c..e44e770ba 100644
--- a/docs/integrations/cursor.mdx
+++ b/docs/integrations/cursor.mdx
@@ -1,5 +1,5 @@
---
-title: Cursor + FastMCP
+title: Cursor 🤝 FastMCP
sidebarTitle: Cursor
description: Install and use FastMCP servers in Cursor
icon: message-smile
@@ -7,13 +7,12 @@ tag: NEW
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { LocalFocusTip } from "/snippets/local-focus.mdx"
+
+
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.
-
-This guide focuses specifically on installing local FastMCP server files directly into Cursor using STDIO transport. For deploying remote servers using SSE or HTTP transports, see the [Cursor MCP documentation](https://docs.cursor.com/context/mcp).
-
-
## Requirements
This integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly in Cursor's settings.
@@ -106,6 +105,24 @@ fastmcp install cursor server.py --name "Weather Server" --env-file .env
**`uv` must be installed and available in your system PATH**. Cursor runs in its own isolated environment and needs `uv` to manage dependencies.
+### Generate MCP JSON
+
+
+**Use the first-class integration above for the best experience.** The MCP JSON generation is useful for advanced use cases, manual configuration, or integration with other tools.
+
+
+You can generate MCP JSON configuration for manual use:
+
+```bash
+# Generate configuration and output to stdout
+fastmcp install mcp-json server.py --name "Dice Roller" --with pandas
+
+# Copy configuration to clipboard for easy pasting
+fastmcp install mcp-json server.py --name "Dice Roller" --copy
+```
+
+This generates the standard `mcpServers` configuration format that can be used with any MCP-compatible client.
+
### Manual Configuration
For more control over the configuration, you can manually edit Cursor's configuration file. The configuration file is located at:
diff --git a/docs/integrations/eunomia-authorization.mdx b/docs/integrations/eunomia-authorization.mdx
index c3d3ce12b..e18ff0779 100644
--- a/docs/integrations/eunomia-authorization.mdx
+++ b/docs/integrations/eunomia-authorization.mdx
@@ -1,8 +1,8 @@
---
-title: Eunomia Authorization + FastMCP
+title: Eunomia Authorization 🤝 FastMCP
sidebarTitle: Eunomia Auth
description: Add policy-based authorization to your FastMCP servers
-icon: layer-group
+icon: shield-check
tag: NEW
---
diff --git a/docs/integrations/gemini.mdx b/docs/integrations/gemini.mdx
index 17e1c0eec..37c37fd72 100644
--- a/docs/integrations/gemini.mdx
+++ b/docs/integrations/gemini.mdx
@@ -1,8 +1,8 @@
---
-title: Gemini SDK + FastMCP
+title: Gemini SDK 🤝 FastMCP
sidebarTitle: Gemini SDK
description: Call FastMCP servers from the Google Gemini SDK
-icon: message-smile
+icon: message-code
---
import { VersionBadge } from "/snippets/version-badge.mdx"
diff --git a/docs/integrations/mcp-json-configuration.mdx b/docs/integrations/mcp-json-configuration.mdx
new file mode 100644
index 000000000..a713f7d7b
--- /dev/null
+++ b/docs/integrations/mcp-json-configuration.mdx
@@ -0,0 +1,347 @@
+---
+title: MCP JSON Configuration 🤝 FastMCP
+sidebarTitle: MCP JSON
+description: Generate standard MCP configuration files for any compatible client
+icon: brackets-curly
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+FastMCP can generate standard MCP JSON configuration files that work with any MCP-compatible client including Claude Desktop, VS Code, Cursor, and other applications that support the Model Context Protocol.
+
+## MCP JSON Configuration Standard
+
+The MCP JSON configuration format is an **emergent standard** that has developed across the MCP ecosystem. This format defines how MCP clients should configure and launch MCP servers, providing a consistent way to specify server commands, arguments, and environment variables.
+
+### Configuration Structure
+
+The standard uses a `mcpServers` object where each key represents a server name and the value contains the server's configuration:
+
+```json
+{
+ "mcpServers": {
+ "server-name": {
+ "command": "executable",
+ "args": ["arg1", "arg2"],
+ "env": {
+ "VAR": "value"
+ }
+ }
+ }
+}
+```
+
+### Server Configuration Fields
+
+#### `command` (required)
+The executable command to run the MCP server. This should be an absolute path or a command available in the system PATH.
+
+```json
+{
+ "command": "python"
+}
+```
+
+#### `args` (optional)
+An array of command-line arguments passed to the server executable. Arguments are passed in order.
+
+```json
+{
+ "args": ["server.py", "--verbose", "--port", "8080"]
+}
+```
+
+#### `env` (optional)
+An object containing environment variables to set when launching the server. All values must be strings.
+
+```json
+{
+ "env": {
+ "API_KEY": "secret-key",
+ "DEBUG": "true",
+ "PORT": "8080"
+ }
+}
+```
+
+### Client Adoption
+
+This format is widely adopted across the MCP ecosystem:
+
+- **Claude Desktop**: Uses `~/.claude/claude_desktop_config.json`
+- **Cursor**: Uses `~/.cursor/mcp.json`
+- **VS Code**: Uses workspace `.vscode/mcp.json`
+- **Other clients**: Many MCP-compatible applications follow this standard
+
+## Overview
+
+
+**For the best experience, use FastMCP's first-class integrations:** [`fastmcp install claude-code`](/integrations/claude-code), [`fastmcp install claude-desktop`](/integrations/claude-desktop), or [`fastmcp install cursor`](/integrations/cursor). Use MCP JSON generation for advanced use cases and unsupported clients.
+
+
+The `fastmcp install mcp-json` command generates configuration in the standard `mcpServers` format used across the MCP ecosystem. This is useful when:
+
+- **Working with unsupported clients** - Any MCP client not directly integrated with FastMCP
+- **CI/CD environments** - Automated configuration generation for deployments
+- **Configuration sharing** - Easy distribution of server setups to team members
+- **Custom tooling** - Integration with your own MCP management tools
+- **Manual setup** - When you prefer to manually configure your MCP client
+
+## Basic Usage
+
+Generate configuration and output to stdout (useful for piping):
+
+```bash
+fastmcp install mcp-json server.py
+```
+
+This outputs the server configuration JSON that you add to the `mcpServers` object:
+
+```json
+{
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/server.py"
+ ]
+}
+```
+
+To use this in a client configuration file, add it under a server name in the `mcpServers` object:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/server.py"
+ ]
+ }
+ }
+}
+```
+
+## Configuration Options
+
+### Server Naming
+
+```bash
+# Use server's built-in name (from FastMCP constructor)
+fastmcp install mcp-json server.py
+
+# Override with custom name
+fastmcp install mcp-json server.py --name "Custom Server Name"
+```
+
+### Dependencies
+
+Add Python packages your server needs:
+
+```bash
+# Single package
+fastmcp install mcp-json server.py --with pandas
+
+# Multiple packages
+fastmcp install mcp-json server.py --with pandas --with requests --with httpx
+
+# Editable local package
+fastmcp install mcp-json server.py --with-editable ./my-package
+```
+
+You can also specify dependencies directly in your server code:
+
+```python server.py
+from fastmcp import FastMCP
+
+mcp = FastMCP(
+ name="Data Analysis Server",
+ dependencies=["pandas", "matplotlib", "seaborn"]
+)
+```
+
+### Environment Variables
+
+```bash
+# Individual environment variables
+fastmcp install mcp-json server.py \
+ --env-var API_KEY=your-secret-key \
+ --env-var DEBUG=true
+
+# Load from .env file
+fastmcp install mcp-json server.py --env-file .env
+```
+
+### Server Object Selection
+
+Use the same `file.py:object` notation as other FastMCP commands:
+
+```bash
+# Auto-detects server object (looks for 'mcp', 'server', or 'app')
+fastmcp install mcp-json server.py
+
+# Explicit server object
+fastmcp install mcp-json server.py:my_custom_server
+```
+
+## Clipboard Integration
+
+Copy configuration directly to your clipboard for easy pasting:
+
+```bash
+fastmcp install mcp-json server.py --copy
+```
+
+
+The `--copy` flag requires the `pyperclip` Python package. If not installed, you'll see an error message with installation instructions.
+
+
+## Usage Examples
+
+### Basic Server
+
+```bash
+fastmcp install mcp-json dice_server.py
+```
+
+Output:
+```json
+{
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/home/user/dice_server.py"
+ ]
+}
+```
+
+### Production Server with Dependencies
+
+```bash
+fastmcp install mcp-json api_server.py \
+ --name "Production API Server" \
+ --with requests \
+ --with python-dotenv \
+ --env-var API_BASE_URL=https://api.example.com \
+ --env-var TIMEOUT=30
+```
+
+Output:
+```json
+{
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "--with",
+ "python-dotenv",
+ "--with",
+ "requests",
+ "fastmcp",
+ "run",
+ "/home/user/api_server.py"
+ ],
+ "env": {
+ "API_BASE_URL": "https://api.example.com",
+ "TIMEOUT": "30"
+ }
+}
+```
+
+### Pipeline Usage
+
+Save configuration to file:
+
+```bash
+fastmcp install mcp-json server.py > mcp-config.json
+```
+
+Use in shell scripts:
+
+```bash
+#!/bin/bash
+CONFIG=$(fastmcp install mcp-json server.py --name "CI Server")
+echo "$CONFIG" | jq '.command'
+# Output: "uv"
+```
+
+## Integration with MCP Clients
+
+The generated configuration works with any MCP-compatible application:
+
+### Claude Desktop
+
+**Prefer [`fastmcp install claude-desktop`](/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs.
+
+Copy the `mcpServers` object into `~/.claude/claude_desktop_config.json`
+
+### Cursor
+
+**Prefer [`fastmcp install cursor`](/integrations/cursor)** for automatic installation. Use MCP JSON for advanced configuration needs.
+
+Add to `~/.cursor/mcp.json`
+
+### VS Code
+Add to your workspace's `.vscode/mcp.json` file
+
+### Custom Applications
+Use the JSON configuration with any application that supports the MCP protocol
+
+## Configuration Format
+
+The generated configuration follows the standard MCP server specification:
+
+```json
+{
+ "mcpServers": {
+ "": {
+ "command": "",
+ "args": ["", "", "..."],
+ "env": {
+ "": ""
+ }
+ }
+ }
+}
+```
+
+**Fields:**
+- `command`: The executable to run (always `uv` for FastMCP servers)
+- `args`: Command-line arguments including dependencies and server path
+- `env`: Environment variables (only included if specified)
+
+
+**All file paths in the generated configuration are absolute paths**. This ensures the configuration works regardless of the working directory when the MCP client starts the server.
+
+
+## Requirements
+
+- **uv**: Must be installed and available in your system PATH
+- **pyperclip** (optional): Required only for `--copy` functionality
+
+Install uv if not already available:
+
+```bash
+# macOS
+brew install uv
+
+# Linux/Windows
+curl -LsSf https://astral.sh/uv/install.sh | sh
+```
\ No newline at end of file
diff --git a/docs/integrations/openai.mdx b/docs/integrations/openai.mdx
index 2d1940b9b..3bbbe3d8b 100644
--- a/docs/integrations/openai.mdx
+++ b/docs/integrations/openai.mdx
@@ -1,8 +1,8 @@
---
-title: OpenAI API + FastMCP
+title: OpenAI API 🤝 FastMCP
sidebarTitle: OpenAI API
description: Call FastMCP servers from the OpenAI API
-icon: message-smile
+icon: message-code
tag: NEW
---
diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx
index f73b821de..f3833abb1 100644
--- a/docs/patterns/cli.mdx
+++ b/docs/patterns/cli.mdx
@@ -151,11 +151,13 @@ Install a MCP server in MCP client applications. FastMCP currently supports the
- **Claude Code** - Installs via Claude Code's built-in MCP management system
- **Claude Desktop** - Installs via direct configuration file modification
- **Cursor** - Installs via deeplink that opens Cursor for user confirmation
+- **MCP JSON** - Generates standard MCP JSON configuration for manual use
```bash
fastmcp install claude-code server.py
fastmcp install claude-desktop server.py
fastmcp install cursor server.py
+fastmcp install mcp-json server.py
```
Note that for security reasons, MCP clients run every server in a completely isolated environment. Therefore, all dependencies must be explicitly specified using the `--with` and/or `--with-editable` options (following `uv` conventions) or by attaching them to your server in code via the `dependencies` parameter.
@@ -164,9 +166,9 @@ Note that for security reasons, MCP clients run every server in a completely iso
**`uv` must be installed and available in your system PATH**. Both Claude Desktop and Cursor run in isolated environments and need `uv` to manage dependencies. On macOS, install `uv` globally with Homebrew for Claude Desktop compatibility: `brew install uv`.
-
-The `install` command currently only sets up servers for STDIO transport. When installed in MCP client applications, your server will be run using STDIO regardless of any transport configuration in your code.
-
+
+**FastMCP `install` commands focus on local server files with STDIO transport.** For remote servers running with HTTP or SSE transport, use your client's native configuration - FastMCP's value is simplifying the complex local setup with dependencies and `uv` commands.
+
#### Server Specification
@@ -205,8 +207,52 @@ fastmcp install cursor server.py --env-var API_KEY=secret --env-var DEBUG=true
# Install with environment file
fastmcp install cursor server.py --env-file .env
+
+# Generate MCP JSON configuration
+fastmcp install mcp-json server.py --name "My Server" --with pandas
+
+# Copy JSON configuration to clipboard
+fastmcp install mcp-json server.py --copy
```
+#### MCP JSON Generation
+
+The `mcp-json` subcommand generates standard MCP JSON configuration that can be used with any MCP-compatible client. This is useful when:
+
+- Working with MCP clients not directly supported by FastMCP
+- Creating configuration for CI/CD environments
+- Sharing server configurations with others
+- Integration with custom tooling
+
+The generated JSON follows the standard `mcpServers` format used by Claude Desktop, VS Code, Cursor, and other MCP clients:
+
+```json
+{
+ "mcpServers": {
+ "server-name": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/path/to/server.py"
+ ],
+ "env": {
+ "API_KEY": "value"
+ }
+ }
+ }
+}
+```
+
+**Options specific to mcp-json:**
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Copy to Clipboard | `--copy` | Copy configuration to clipboard instead of printing to stdout |
+
### `inspect`
diff --git a/docs/integrations/contrib.mdx b/docs/patterns/contrib.mdx
similarity index 100%
rename from docs/integrations/contrib.mdx
rename to docs/patterns/contrib.mdx
diff --git a/docs/patterns/tool-transformation.mdx b/docs/patterns/tool-transformation.mdx
index c9528282e..17462fd50 100644
--- a/docs/patterns/tool-transformation.mdx
+++ b/docs/patterns/tool-transformation.mdx
@@ -3,7 +3,6 @@ title: Tool Transformation
sidebarTitle: Tool Transformation
description: Create enhanced tool variants with modified schemas, argument mappings, and custom behavior.
icon: wand-magic-sparkles
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
diff --git a/docs/snippets/local-focus.mdx b/docs/snippets/local-focus.mdx
new file mode 100644
index 000000000..4da6ac752
--- /dev/null
+++ b/docs/snippets/local-focus.mdx
@@ -0,0 +1,7 @@
+export const LocalFocusTip = () => {
+ return (
+
+ This integration focuses on running local FastMCP server files with STDIO transport. For remote servers running with HTTP or SSE transport, use your client's native configuration - FastMCP's integrations focus on simplifying the complex local setup with dependencies and uv commands.
+
+ );
+};
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index d4fbbed4e..f5751427f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,7 @@ dev = [
"pdbpp>=0.10.3",
"pre-commit",
"pyinstrument>=5.0.2",
+ "pyperclip>=1.9.0",
"pyright>=1.1.389",
"pytest>=8.3.3",
"pytest-asyncio>=0.23.5",
diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py
index 5715cf57f..0be4d0ffe 100644
--- a/src/fastmcp/cli/cli.py
+++ b/src/fastmcp/cli/cli.py
@@ -18,7 +18,7 @@ from typer import Context, Exit
import fastmcp
from fastmcp.cli import run as run_module
-from fastmcp.cli.install.install import install
+from fastmcp.cli.install import install_app
from fastmcp.server.server import FastMCP
from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp
from fastmcp.utilities.logging import get_logger
@@ -323,8 +323,8 @@ def run(
sys.exit(1)
-# Add install command directly
-app.command()(install)
+# Add install subcommands
+app.add_typer(install_app)
@app.command()
diff --git a/src/fastmcp/cli/install/__init__.py b/src/fastmcp/cli/install/__init__.py
index aa09f0165..0ccd43606 100644
--- a/src/fastmcp/cli/install/__init__.py
+++ b/src/fastmcp/cli/install/__init__.py
@@ -1,5 +1,27 @@
-"""Install module for FastMCP CLI."""
+"""Install subcommands for FastMCP CLI."""
-from .install import install
+import typer
-__all__ = ["install"]
+from .claude_code import claude_code_command
+from .claude_desktop import claude_desktop_command
+from .cursor import cursor_command
+from .mcp_config import mcp_config_command
+
+# Create a typer app for install subcommands
+install_app = typer.Typer(
+ name="install",
+ help="Install MCP servers in various clients and formats",
+ no_args_is_help=True,
+)
+
+# Register each command from its respective module
+install_app.command("claude-code", help="Install a MCP server in Claude Code")(
+ claude_code_command
+)
+install_app.command("claude-desktop", help="Install a MCP server in Claude Desktop")(
+ claude_desktop_command
+)
+install_app.command("cursor", help="Install a MCP server in Cursor")(cursor_command)
+install_app.command(
+ "mcp-json", help="Generate MCP JSON configuration for manual installation"
+)(mcp_config_command)
diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py
index fd33b4e25..df3eb5c0c 100644
--- a/src/fastmcp/cli/install/claude_code.py
+++ b/src/fastmcp/cli/install/claude_code.py
@@ -3,12 +3,17 @@
from __future__ import annotations
import subprocess
+import sys
from pathlib import Path
+from typing import Annotated
+import typer
from rich import print
from fastmcp.utilities.logging import get_logger
+from .shared import process_common_args
+
logger = get_logger(__name__)
@@ -116,3 +121,73 @@ def install_claude_code(
except Exception as e:
print(f"[red]Failed to install '[bold]{name}[/bold]' in Claude Code: {e}[/red]")
return False
+
+
+def claude_code_command(
+ server_spec: Annotated[
+ str, typer.Argument(help="Python file to run, optionally with :object suffix")
+ ],
+ server_name: Annotated[
+ str | None,
+ typer.Option(
+ "--name",
+ "-n",
+ help="Custom name for the server (defaults to server's name attribute or file name)",
+ ),
+ ] = None,
+ with_editable: Annotated[
+ Path | None,
+ typer.Option(
+ "--with-editable",
+ "-e",
+ help="Directory containing pyproject.toml to install in editable mode",
+ exists=True,
+ file_okay=False,
+ resolve_path=True,
+ ),
+ ] = None,
+ with_packages: Annotated[
+ list[str],
+ typer.Option(
+ "--with", help="Additional packages to install, in PEP 508 format"
+ ),
+ ] = [],
+ env_vars: Annotated[
+ list[str],
+ typer.Option(
+ "--env-var", "-v", help="Environment variables in KEY=VALUE format"
+ ),
+ ] = [],
+ env_file: Annotated[
+ Path | None,
+ typer.Option(
+ "--env-file",
+ "-f",
+ help="Load environment variables from a .env file",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ resolve_path=True,
+ ),
+ ] = None,
+) -> None:
+ """Install a MCP server in Claude Code."""
+ file, server_object, name, packages, env_dict = process_common_args(
+ server_spec, server_name, with_packages, env_vars, env_file
+ )
+
+ success = install_claude_code(
+ file=file,
+ server_object=server_object,
+ name=name,
+ with_editable=with_editable,
+ with_packages=packages,
+ env_vars=env_dict,
+ )
+
+ if success:
+ print(
+ f"[green bold]Successfully installed '[bold]{name}[/bold]' in Claude Code[/green bold]"
+ )
+ else:
+ sys.exit(1)
diff --git a/src/fastmcp/cli/install/claude_desktop.py b/src/fastmcp/cli/install/claude_desktop.py
index 409909f2d..f60eb8301 100644
--- a/src/fastmcp/cli/install/claude_desktop.py
+++ b/src/fastmcp/cli/install/claude_desktop.py
@@ -5,12 +5,16 @@ from __future__ import annotations
import os
import sys
from pathlib import Path
+from typing import Annotated
+import typer
from rich import print
from fastmcp.mcp_config import StdioMCPServer, update_config_file
from fastmcp.utilities.logging import get_logger
+from .shared import process_common_args
+
logger = get_logger(__name__)
@@ -119,3 +123,73 @@ def install_claude_desktop(
f"[red]Failed to install '[bold]{name}[/bold]' in Claude Desktop: {e}[/red]"
)
return False
+
+
+def claude_desktop_command(
+ server_spec: Annotated[
+ str, typer.Argument(help="Python file to run, optionally with :object suffix")
+ ],
+ server_name: Annotated[
+ str | None,
+ typer.Option(
+ "--name",
+ "-n",
+ help="Custom name for the server (defaults to server's name attribute or file name)",
+ ),
+ ] = None,
+ with_editable: Annotated[
+ Path | None,
+ typer.Option(
+ "--with-editable",
+ "-e",
+ help="Directory containing pyproject.toml to install in editable mode",
+ exists=True,
+ file_okay=False,
+ resolve_path=True,
+ ),
+ ] = None,
+ with_packages: Annotated[
+ list[str],
+ typer.Option(
+ "--with", help="Additional packages to install, in PEP 508 format"
+ ),
+ ] = [],
+ env_vars: Annotated[
+ list[str],
+ typer.Option(
+ "--env-var", "-v", help="Environment variables in KEY=VALUE format"
+ ),
+ ] = [],
+ env_file: Annotated[
+ Path | None,
+ typer.Option(
+ "--env-file",
+ "-f",
+ help="Load environment variables from a .env file",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ resolve_path=True,
+ ),
+ ] = None,
+) -> None:
+ """Install a MCP server in Claude Desktop."""
+ file, server_object, name, packages, env_dict = process_common_args(
+ server_spec, server_name, with_packages, env_vars, env_file
+ )
+
+ success = install_claude_desktop(
+ file=file,
+ server_object=server_object,
+ name=name,
+ with_editable=with_editable,
+ with_packages=packages,
+ env_vars=env_dict,
+ )
+
+ if success:
+ print(
+ f"[green bold]Successfully installed '[bold]{name}[/bold]' in Claude Desktop[/green bold]"
+ )
+ else:
+ sys.exit(1)
diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py
index f9f685b07..48e7c36b9 100644
--- a/src/fastmcp/cli/install/cursor.py
+++ b/src/fastmcp/cli/install/cursor.py
@@ -6,12 +6,16 @@ import base64
import subprocess
import sys
from pathlib import Path
+from typing import Annotated
+import typer
from rich import print
from fastmcp.mcp_config import StdioMCPServer
from fastmcp.utilities.logging import get_logger
+from .shared import process_common_args
+
logger = get_logger(__name__)
@@ -133,3 +137,70 @@ def install_cursor(
except Exception as e:
print(f"[red]Failed to generate Cursor deeplink: {e}[/red]")
return False
+
+
+def cursor_command(
+ server_spec: Annotated[
+ str, typer.Argument(help="Python file to run, optionally with :object suffix")
+ ],
+ server_name: Annotated[
+ str | None,
+ typer.Option(
+ "--name",
+ "-n",
+ help="Custom name for the server (defaults to server's name attribute or file name)",
+ ),
+ ] = None,
+ with_editable: Annotated[
+ Path | None,
+ typer.Option(
+ "--with-editable",
+ "-e",
+ help="Directory containing pyproject.toml to install in editable mode",
+ exists=True,
+ file_okay=False,
+ resolve_path=True,
+ ),
+ ] = None,
+ with_packages: Annotated[
+ list[str],
+ typer.Option(
+ "--with", help="Additional packages to install, in PEP 508 format"
+ ),
+ ] = [],
+ env_vars: Annotated[
+ list[str],
+ typer.Option(
+ "--env-var", "-v", help="Environment variables in KEY=VALUE format"
+ ),
+ ] = [],
+ env_file: Annotated[
+ Path | None,
+ typer.Option(
+ "--env-file",
+ "-f",
+ help="Load environment variables from a .env file",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ resolve_path=True,
+ ),
+ ] = None,
+) -> None:
+ """Install a MCP server in Cursor."""
+ file, server_object, name, packages, env_dict = process_common_args(
+ server_spec, server_name, with_packages, env_vars, env_file
+ )
+
+ success = install_cursor(
+ file=file,
+ server_object=server_object,
+ name=name,
+ with_editable=with_editable,
+ with_packages=packages,
+ env_vars=env_dict,
+ )
+
+ # Cursor handles its own messaging, no generic success message needed
+ if not success:
+ sys.exit(1)
diff --git a/src/fastmcp/cli/install/install.py b/src/fastmcp/cli/install/install.py
deleted file mode 100644
index 89de48b66..000000000
--- a/src/fastmcp/cli/install/install.py
+++ /dev/null
@@ -1,198 +0,0 @@
-"""Main install logic for FastMCP CLI."""
-
-from __future__ import annotations
-
-import sys
-from enum import Enum
-from pathlib import Path
-from typing import Annotated
-
-import typer
-from dotenv import dotenv_values
-from rich import print
-
-from fastmcp.cli.run import import_server, parse_file_path
-from fastmcp.utilities.logging import get_logger
-
-from .claude_code import install_claude_code
-from .claude_desktop import install_claude_desktop
-from .cursor import install_cursor
-
-logger = get_logger(__name__)
-
-
-class Client(str, Enum):
- """Supported MCP clients."""
-
- CLAUDE_CODE = "claude-code"
- CLAUDE_DESKTOP = "claude-desktop"
- CURSOR = "cursor"
-
-
-def install(
- client: Annotated[
- Client,
- typer.Argument(help="MCP client to install the server into"),
- ],
- server_spec: Annotated[
- str, typer.Argument(help="Python file to run, optionally with :object suffix")
- ],
- server_name: Annotated[
- str | None,
- typer.Option(
- "--name",
- "-n",
- help="Custom name for the server (defaults to server's name attribute or file name)",
- ),
- ] = None,
- with_editable: Annotated[
- Path | None,
- typer.Option(
- "--with-editable",
- "-e",
- help="Directory containing pyproject.toml to install in editable mode. Use this to include local packages that are not available on PyPI.",
- exists=True,
- file_okay=False,
- resolve_path=True,
- ),
- ] = None,
- with_packages: Annotated[
- list[str],
- typer.Option(
- "--with",
- help="Additional packages to install, in PEP 508 format (e.g. 'httpx>=0.25.2')",
- ),
- ] = [],
- env_vars: Annotated[
- list[str],
- typer.Option(
- "--env-var",
- "-v",
- help="Environment variables in KEY=VALUE format",
- ),
- ] = [],
- env_file: Annotated[
- Path | None,
- typer.Option(
- "--env-file",
- "-f",
- help="Load environment variables from a .env file",
- exists=True,
- file_okay=True,
- dir_okay=False,
- resolve_path=True,
- ),
- ] = None,
-) -> None:
- """Install a MCP server in the specified target application.
-
- Environment variables are preserved once added and only updated if new values
- are explicitly provided.
- """
-
- # Parse server spec
- file, server_object = parse_file_path(server_spec)
-
- logger.debug(
- "Installing server",
- extra={
- "client": client,
- "file": str(file),
- "server_name": server_name,
- "server_object": server_object,
- "with_editable": str(with_editable) if with_editable else None,
- "with_packages": with_packages,
- },
- )
-
- # Try to import server to get its name and dependencies
- name = server_name
- server = None
- if not name:
- try:
- server = import_server(file, server_object)
- name = server.name
- except (ImportError, ModuleNotFoundError) as e:
- logger.debug(
- "Could not import server (likely missing dependencies), using file name",
- extra={"error": str(e)},
- )
- name = file.stem
-
- # Get server dependencies if available
- server_dependencies = getattr(server, "dependencies", []) if server else []
- if server_dependencies:
- with_packages = list(set(with_packages + server_dependencies))
-
- # Process environment variables if provided
- env_dict: dict[str, str] | None = None
- if env_file or env_vars:
- env_dict = {}
- # Load from .env file if specified
- if env_file:
- try:
- env_dict |= {
- k: v for k, v in dotenv_values(env_file).items() if v is not None
- }
- except Exception as e:
- print(f"[red]❌ Failed to load .env file: {e}[/red]")
- sys.exit(1)
-
- # Add command line environment variables
- for env_var in env_vars:
- key, value = _parse_env_var(env_var)
- env_dict[key] = value
-
- # Route to appropriate installer
- if client == Client.CLAUDE_CODE:
- success = install_claude_code(
- file=file,
- server_object=server_object,
- name=name,
- with_editable=with_editable,
- with_packages=with_packages,
- env_vars=env_dict,
- )
- elif client == Client.CLAUDE_DESKTOP:
- success = install_claude_desktop(
- file=file,
- server_object=server_object,
- name=name,
- with_editable=with_editable,
- with_packages=with_packages,
- env_vars=env_dict,
- )
- elif client == Client.CURSOR:
- success = install_cursor(
- file=file,
- server_object=server_object,
- name=name,
- with_editable=with_editable,
- with_packages=with_packages,
- env_vars=env_dict,
- )
- else:
- print(
- f"[red bold]Unknown client: {client!r}[/red bold]. Supported clients: [bold]{Client.CLAUDE_CODE}[/bold], [bold]{Client.CLAUDE_DESKTOP}[/bold], [bold]{Client.CURSOR}[/bold]"
- )
- raise typer.Exit(1)
-
- if success:
- # Only show generic success message for clients that don't have their own messaging
- if client != Client.CURSOR:
- print(
- f"[green bold]Successfully installed '[bold]{name}[/bold]' in {client.value}[/green bold]"
- )
- else:
- sys.exit(1)
-
-
-def _parse_env_var(env_var: str) -> tuple[str, str]:
- """Parse environment variable string in format KEY=VALUE."""
- if "=" not in env_var:
- print(
- f"[red]❌ Invalid environment variable format: '[bold]{env_var}[/bold]'. Must be KEY=VALUE[/red]"
- )
- sys.exit(1)
- key, value = env_var.split("=", 1)
- return key.strip(), value.strip()
diff --git a/src/fastmcp/cli/install/mcp_config.py b/src/fastmcp/cli/install/mcp_config.py
new file mode 100644
index 000000000..8eee79a86
--- /dev/null
+++ b/src/fastmcp/cli/install/mcp_config.py
@@ -0,0 +1,179 @@
+"""MCP configuration JSON generation for FastMCP install."""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+from typing import Annotated
+
+import typer
+from rich import print
+
+from fastmcp.utilities.logging import get_logger
+
+from .shared import process_common_args
+
+logger = get_logger(__name__)
+
+
+def install_mcp_config(
+ file: Path,
+ server_object: str | None,
+ name: str,
+ *,
+ with_editable: Path | None = None,
+ with_packages: list[str] | None = None,
+ env_vars: dict[str, str] | None = None,
+ copy: bool = False,
+) -> bool:
+ """Generate MCP configuration JSON for manual installation.
+
+ Args:
+ file: Path to the server file
+ server_object: Optional server object name (for :object suffix)
+ name: Name for the server in MCP config
+ with_editable: Optional directory to install in editable mode
+ with_packages: Optional list of additional packages to install
+ env_vars: Optional dictionary of environment variables
+ copy: If True, copy to clipboard instead of printing to stdout
+
+ Returns:
+ True if generation was successful, False otherwise
+ """
+ try:
+ # Build uv run command
+ args = ["run"]
+
+ # Collect all packages in a set to deduplicate
+ packages = {"fastmcp"}
+ if with_packages:
+ packages.update(pkg for pkg in with_packages if pkg)
+
+ # Add all packages with --with
+ for pkg in sorted(packages):
+ args.extend(["--with", pkg])
+
+ if with_editable:
+ args.extend(["--with-editable", str(with_editable)])
+
+ # Build server spec from parsed components
+ if server_object:
+ server_spec = f"{file.resolve()}:{server_object}"
+ else:
+ server_spec = str(file.resolve())
+
+ # Add fastmcp run command
+ args.extend(["fastmcp", "run", server_spec])
+
+ # Build MCP server configuration (just the server object, not the wrapper)
+ config = {
+ "command": "uv",
+ "args": args,
+ }
+
+ # Add environment variables if provided
+ if env_vars:
+ config["env"] = env_vars
+
+ # Convert to JSON
+ json_output = json.dumps(config, indent=2)
+
+ # Handle output
+ if copy:
+ try:
+ import pyperclip
+
+ pyperclip.copy(json_output)
+ print(
+ f"[green]MCP configuration for '[bold]{name}[/bold]' copied to clipboard[/green]"
+ )
+ except ImportError:
+ print(
+ "[red]The `--copy` flag requires pyperclip. Please install pyperclip and try again: `pip install pyperclip`[/red]"
+ )
+ return False
+ else:
+ # Print to stdout (for piping)
+ print(json_output)
+
+ return True
+
+ except Exception as e:
+ print(f"[red]❌ Failed to generate MCP configuration: {e}[/red]")
+ return False
+
+
+def mcp_config_command(
+ server_spec: Annotated[
+ str, typer.Argument(help="Python file to run, optionally with :object suffix")
+ ],
+ server_name: Annotated[
+ str | None,
+ typer.Option(
+ "--name",
+ "-n",
+ help="Custom name for the server (defaults to server's name attribute or file name)",
+ ),
+ ] = None,
+ with_editable: Annotated[
+ Path | None,
+ typer.Option(
+ "--with-editable",
+ "-e",
+ help="Directory containing pyproject.toml to install in editable mode",
+ exists=True,
+ file_okay=False,
+ resolve_path=True,
+ ),
+ ] = None,
+ with_packages: Annotated[
+ list[str],
+ typer.Option(
+ "--with", help="Additional packages to install, in PEP 508 format"
+ ),
+ ] = [],
+ env_vars: Annotated[
+ list[str],
+ typer.Option(
+ "--env-var", "-v", help="Environment variables in KEY=VALUE format"
+ ),
+ ] = [],
+ env_file: Annotated[
+ Path | None,
+ typer.Option(
+ "--env-file",
+ "-f",
+ help="Load environment variables from a .env file",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ resolve_path=True,
+ ),
+ ] = None,
+ copy: Annotated[
+ bool,
+ typer.Option(
+ "--copy",
+ help="Copy configuration to clipboard instead of printing to stdout",
+ ),
+ ] = False,
+) -> None:
+ """Generate MCP configuration JSON for manual installation."""
+ file, server_object, name, packages, env_dict = process_common_args(
+ server_spec, server_name, with_packages, env_vars, env_file
+ )
+
+ success = install_mcp_config(
+ file=file,
+ server_object=server_object,
+ name=name,
+ with_editable=with_editable,
+ with_packages=packages,
+ env_vars=env_dict,
+ copy=copy,
+ )
+
+ # mcp-config handles its own messaging, no generic success message needed
+ if not success:
+ sys.exit(1)
diff --git a/src/fastmcp/cli/install/shared.py b/src/fastmcp/cli/install/shared.py
new file mode 100644
index 000000000..a6553c4f9
--- /dev/null
+++ b/src/fastmcp/cli/install/shared.py
@@ -0,0 +1,87 @@
+"""Shared utilities for install commands."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+from dotenv import dotenv_values
+from rich import print
+
+from fastmcp.cli.run import import_server, parse_file_path
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+def parse_env_var(env_var: str) -> tuple[str, str]:
+ """Parse environment variable string in format KEY=VALUE."""
+ if "=" not in env_var:
+ print(
+ f"[red]❌ Invalid environment variable format: '[bold]{env_var}[/bold]'. Must be KEY=VALUE[/red]"
+ )
+ sys.exit(1)
+ key, value = env_var.split("=", 1)
+ return key.strip(), value.strip()
+
+
+def process_common_args(
+ server_spec: str,
+ server_name: str | None,
+ with_packages: list[str],
+ env_vars: list[str],
+ env_file: Path | None,
+) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:
+ """Process common arguments shared by all install commands."""
+ # Parse server spec
+ file, server_object = parse_file_path(server_spec)
+
+ logger.debug(
+ "Installing server",
+ extra={
+ "file": str(file),
+ "server_name": server_name,
+ "server_object": server_object,
+ "with_packages": with_packages,
+ },
+ )
+
+ # Try to import server to get its name and dependencies
+ name = server_name
+ server = None
+ if not name:
+ try:
+ server = import_server(file, server_object)
+ name = server.name
+ except (ImportError, ModuleNotFoundError) as e:
+ logger.debug(
+ "Could not import server (likely missing dependencies), using file name",
+ extra={"error": str(e)},
+ )
+ name = file.stem
+
+ # Get server dependencies if available
+ server_dependencies = getattr(server, "dependencies", []) if server else []
+ if server_dependencies:
+ with_packages = list(set(with_packages + server_dependencies))
+
+ # Process environment variables if provided
+ env_dict: dict[str, str] | None = None
+ if env_file or env_vars:
+ env_dict = {}
+ # Load from .env file if specified
+ if env_file:
+ try:
+ env_dict |= {
+ k: v for k, v in dotenv_values(env_file).items() if v is not None
+ }
+ except Exception as e:
+ print(f"[red]❌ Failed to load .env file: {e}[/red]")
+ sys.exit(1)
+
+ # Add command line environment variables
+ for env_var in env_vars:
+ key, value = parse_env_var(env_var)
+ env_dict[key] = value
+
+ return file, server_object, name, with_packages, env_dict
diff --git a/tests/cli/test_mcp_config.py b/tests/cli/test_mcp_config.py
new file mode 100644
index 000000000..8eecfe861
--- /dev/null
+++ b/tests/cli/test_mcp_config.py
@@ -0,0 +1,199 @@
+"""Tests for MCP configuration JSON generation."""
+
+import json
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+from fastmcp.cli.install.mcp_config import install_mcp_config
+
+
+class TestInstallMcpConfig:
+ """Test install_mcp_config function."""
+
+ def test_generates_basic_config(self):
+ """Should generate basic MCP configuration with minimal options."""
+ result = install_mcp_config(
+ file=Path("server.py"),
+ server_object=None,
+ name="test-server",
+ )
+
+ assert result is True
+
+ @patch("fastmcp.cli.install.mcp_config.print")
+ def test_generates_config_with_all_options(self, mock_print):
+ """Should generate MCP configuration with all options."""
+ result = install_mcp_config(
+ file=Path("server.py"),
+ server_object="custom_server",
+ name="test-server",
+ with_editable=Path("/path/to/editable"),
+ with_packages=["pandas", "requests"],
+ env_vars={"API_KEY": "secret", "DEBUG": "true"},
+ )
+
+ assert result is True
+ mock_print.assert_called_once()
+
+ # Get the JSON output from print call
+ json_output = mock_print.call_args[0][0]
+ config = json.loads(json_output)
+
+ # Verify structure (should be just the server config, not wrapped in mcpServers)
+ server_config = config
+
+ # Verify command and args
+ assert server_config["command"] == "uv"
+ assert "run" in server_config["args"]
+ assert "--with" in server_config["args"]
+ assert "fastmcp" in server_config["args"]
+ assert "pandas" in server_config["args"]
+ assert "requests" in server_config["args"]
+ assert "--with-editable" in server_config["args"]
+ assert str(Path("/path/to/editable")) in server_config["args"]
+
+ # Verify server spec with object
+ server_spec_in_args = None
+ for i, arg in enumerate(server_config["args"]):
+ if (
+ arg == "fastmcp"
+ and i + 2 < len(server_config["args"])
+ and server_config["args"][i + 1] == "run"
+ ):
+ server_spec_in_args = server_config["args"][i + 2]
+ break
+
+ assert server_spec_in_args is not None
+ assert ":custom_server" in server_spec_in_args
+
+ # Verify environment variables
+ assert server_config["env"] == {"API_KEY": "secret", "DEBUG": "true"}
+
+ @patch("fastmcp.cli.install.mcp_config.print")
+ def test_generates_config_without_env_vars(self, mock_print):
+ """Should generate MCP configuration without env section when no env vars."""
+ result = install_mcp_config(
+ file=Path("server.py"),
+ server_object=None,
+ name="test-server",
+ )
+
+ assert result is True
+ json_output = mock_print.call_args[0][0]
+ config = json.loads(json_output)
+
+ # Should not have env section
+ assert "env" not in config
+
+ @patch("fastmcp.cli.install.mcp_config.print")
+ def test_deduplicates_packages(self, mock_print):
+ """Should deduplicate packages including fastmcp."""
+ result = install_mcp_config(
+ file=Path("server.py"),
+ server_object=None,
+ name="test-server",
+ with_packages=["pandas", "fastmcp", "pandas"], # duplicates
+ )
+
+ assert result is True
+ json_output = mock_print.call_args[0][0]
+ config = json.loads(json_output)
+
+ args = config["args"]
+
+ # Count occurrences of packages
+ pandas_count = sum(1 for arg in args if arg == "pandas")
+ fastmcp_count = sum(1 for arg in args if arg == "fastmcp")
+
+ # Should only appear once each for the package (fastmcp appears twice: once as package, once as command)
+ assert pandas_count == 1
+ assert fastmcp_count == 2 # Once in --with fastmcp, once in fastmcp run
+
+ @patch("fastmcp.cli.install.mcp_config.print")
+ def test_resolves_absolute_paths(self, mock_print):
+ """Should resolve server file to absolute path."""
+ result = install_mcp_config(
+ file=Path("server.py"),
+ server_object=None,
+ name="test-server",
+ )
+
+ assert result is True
+ json_output = mock_print.call_args[0][0]
+ config = json.loads(json_output)
+
+ args = config["args"]
+
+ # Find the server spec after "fastmcp run"
+ server_spec_in_args = None
+ for i, arg in enumerate(args):
+ if arg == "fastmcp" and i + 2 < len(args) and args[i + 1] == "run":
+ server_spec_in_args = args[i + 2]
+ break
+
+ assert server_spec_in_args is not None
+ assert str(Path("server.py").resolve()) in server_spec_in_args
+
+ @patch("fastmcp.cli.install.mcp_config.print")
+ def test_copy_to_clipboard_success(self, mock_print):
+ """Should copy configuration to clipboard when copy=True."""
+ # Mock the pyperclip module at import time
+ mock_pyperclip = MagicMock()
+ mock_copy = MagicMock()
+ mock_pyperclip.copy = mock_copy
+
+ with patch.dict("sys.modules", {"pyperclip": mock_pyperclip}):
+ result = install_mcp_config(
+ file=Path("server.py"),
+ server_object=None,
+ name="test-server",
+ copy=True,
+ )
+
+ assert result is True
+ mock_copy.assert_called_once()
+
+ # Verify clipboard content is valid JSON
+ clipboard_content = mock_copy.call_args[0][0]
+ config = json.loads(clipboard_content) # Should not raise
+ assert "command" in config # Should be server config, not wrapped
+
+ # Should print success message
+ mock_print.assert_called_once()
+ assert "copied to clipboard" in str(mock_print.call_args)
+
+ @patch("fastmcp.cli.install.mcp_config.print")
+ def test_copy_to_clipboard_import_error(self, mock_print):
+ """Should handle pyperclip import error gracefully."""
+ with patch(
+ "builtins.__import__",
+ side_effect=ImportError("No module named 'pyperclip'"),
+ ):
+ result = install_mcp_config(
+ file=Path("server.py"),
+ server_object=None,
+ name="test-server",
+ copy=True,
+ )
+
+ assert result is False
+
+ # Should print error message
+ mock_print.assert_called_once()
+ error_call = str(mock_print.call_args)
+ assert "copy` flag requires pyperclip" in error_call
+ assert "pip install pyperclip" in error_call
+
+ @patch("fastmcp.cli.install.mcp_config.print")
+ def test_handles_exception_gracefully(self, mock_print):
+ """Should handle unexpected exceptions gracefully."""
+ with patch("json.dumps", side_effect=Exception("JSON error")):
+ result = install_mcp_config(
+ file=Path("server.py"),
+ server_object=None,
+ name="test-server",
+ )
+
+ assert result is False
+ mock_print.assert_called_once()
+ assert "Failed to generate MCP configuration" in str(mock_print.call_args)
diff --git a/uv.lock b/uv.lock
index ebfad50b7..2c84c14ea 100644
--- a/uv.lock
+++ b/uv.lock
@@ -487,6 +487,7 @@ dev = [
{ name = "pdbpp" },
{ name = "pre-commit" },
{ name = "pyinstrument" },
+ { name = "pyperclip" },
{ name = "pyright" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -524,6 +525,7 @@ dev = [
{ name = "pdbpp", specifier = ">=0.10.3" },
{ name = "pre-commit" },
{ name = "pyinstrument", specifier = ">=5.0.2" },
+ { name = "pyperclip", specifier = ">=1.9.0" },
{ name = "pyright", specifier = ">=1.1.389" },
{ name = "pytest", specifier = ">=8.3.3" },
{ name = "pytest-asyncio", specifier = ">=0.23.5" },