Add MCP json output from CLI (#1056)

This commit is contained in:
Jeremiah Lowin 2025-07-06 10:23:50 -04:00 committed by GitHub
commit f9a698dcf9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1163 additions and 232 deletions

View file

@ -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),

View file

@ -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"
]
},
{

View file

@ -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"

View file

@ -1,5 +1,5 @@
---
title: ChatGPT + FastMCP
title: ChatGPT 🤝 FastMCP
sidebarTitle: ChatGPT
description: Connect FastMCP servers to ChatGPT Deep Research
icon: message-smile

View file

@ -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"
<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.
<Note>
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).
</Note>
## 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.

View file

@ -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"
<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.

View file

@ -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"
<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.
<Note>
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).
</Note>
## 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.
</Warning>
### Generate MCP JSON
<Note>
**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.
</Note>
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:

View file

@ -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
---

View file

@ -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"

View file

@ -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"
<VersionBadge version="2.10.3" />
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
<Note>
**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.
</Note>
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
```
<Note>
The `--copy` flag requires the `pyperclip` Python package. If not installed, you'll see an error message with installation instructions.
</Note>
## 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
<Note>
**Prefer [`fastmcp install claude-desktop`](/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs.
</Note>
Copy the `mcpServers` object into `~/.claude/claude_desktop_config.json`
### Cursor
<Note>
**Prefer [`fastmcp install cursor`](/integrations/cursor)** for automatic installation. Use MCP JSON for advanced configuration needs.
</Note>
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": {
"<server-name>": {
"command": "<executable>",
"args": ["<arg1>", "<arg2>", "..."],
"env": {
"<ENV_VAR>": "<value>"
}
}
}
}
```
**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)
<Warning>
**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.
</Warning>
## 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
```

View file

@ -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
---

View file

@ -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`.
</Warning>
<Warning>
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.
</Warning>
<Tip>
**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.
</Tip>
#### 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`
<VersionBadge version="2.9.0" />

View file

@ -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'

View file

@ -0,0 +1,7 @@
export const LocalFocusTip = () => {
return (
<Tip>
<strong>This integration focuses on running local FastMCP server files with STDIO transport.</strong> 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 <code>uv</code> commands.
</Tip>
);
};

View file

@ -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",

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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)

2
uv.lock generated
View file

@ -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" },