Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 75 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ For Claude Desktop, the configuration entry can look like this:
"mcpServers": {
"mcp-proxy": {
"command": "mcp-proxy",
"args": [
"http://example.io/sse"
],
"args": ["http://example.io/sse"],
"env": {
"API_ACCESS_TOKEN": "access-token"
}
Expand Down Expand Up @@ -182,22 +180,20 @@ The JSON file should follow this structure:
"disabled": false,
"timeout": 60,
"command": "uvx",
"args": [
"mcp-server-fetch"
],
"args": ["mcp-server-fetch"],
"transportType": "stdio"
},
"github": {
"timeout": 60,
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-github"
],
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
},
"transportType": "stdio"
"transportType": "stdio",
"headerToEnv": {
"Authorization": "GITHUB_PERSONAL_ACCESS_TOKEN"
}
}
}
}
Expand All @@ -207,8 +203,73 @@ The JSON file should follow this structure:
- `command`: (Required) The command to execute for the stdio server.
- `args`: (Optional) A list of arguments for the command. Defaults to an empty list.
- `enabled`: (Optional) If `false`, this server definition will be skipped. Defaults to `true`.
- `env`: (Optional) Static environment variables to pass to the stdio server.
- `headerToEnv`: (Optional) Dynamic mapping of HTTP headers to environment variables. When a request comes in with the specified header, its value will be passed as an environment variable to the stdio server.
- `timeout` and `transportType`: These fields are present in standard MCP client configurations but are currently **ignored** by `mcp-proxy` when loading named servers. The transport type is implicitly "stdio".

## Dynamic Token Support

`mcp-proxy` supports dynamic token extraction from HTTP headers. This allows you to pass authentication tokens with each request instead of hardcoding them in the configuration.

### How it works

1. **Configure header mapping**: In your server configuration, add a `headerToEnv` section that maps HTTP header names to environment variable names.
2. **Send requests with headers**: When making requests to your MCP servers, include the required headers with your tokens.
3. **Automatic token extraction**: `mcp-proxy` automatically extracts the tokens from headers and passes them as environment variables to the stdio processes.

### Example Usage

**Configuration:**

```json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"headerToEnv": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "GITHUB_PERSONAL_ACCESS_TOKEN"
}
}
}
}
```

**HTTP Request:**

```http
GET /servers/github/sse HTTP/1.1
Host: localhost:8080
GITHUB_PERSONAL_ACCESS_TOKEN: ghp_1234567890abcdef
```

**Result:** The GitHub MCP server will receive `GITHUB_PERSONAL_ACCESS_TOKEN=ghp_1234567890abcdef` as an environment variable.

### Multiple Tokens

You can configure multiple header mappings for a single server:

```json
{
"mcpServers": {
"bitrix": {
"command": "python3",
"args": ["mcp-bitrix/mcp_server.py"],
"headerToEnv": {
"Authorization": "BITRIX_WEBHOOK_URL",
"X-Bitrix-Signature": "BITRIX_ACCESS_TOKEN"
}
}
}
}
```

### Security Considerations

- **Token validation**: Consider implementing token validation in your MCP servers.
- **HTTPS only**: Always use HTTPS in production to protect tokens in transit.
- **Token rotation**: Implement token rotation mechanisms for enhanced security.

## Installation

### Installing via PyPI
Expand Down Expand Up @@ -253,6 +314,7 @@ docker run --rm -t ghcr.io/sparfenyuk/mcp-proxy:v0.3.2-alpine --help

**Solution**: Try to use the full path to the binary. To do so, open a terminal and run the command`which mcp-proxy` (
macOS, Linux) or `where.exe mcp-proxy` (Windows). Then, use the output path as a value for 'command' attribute:

```json
"fetch": {
"command": "/full/path/to/bin/mcp-proxy",
Expand Down Expand Up @@ -374,18 +436,13 @@ Examples:
"enabled": true,
"timeout": 60,
"command": "uvx",
"args": [
"mcp-server-fetch"
],
"args": ["mcp-server-fetch"],
"transportType": "stdio"
},
"github": {
"timeout": 60,
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-github"
],
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
},
Expand Down
37 changes: 34 additions & 3 deletions config_example.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,45 @@
"transportType": "stdio"
},
"github": {
"enabled": true,
"timeout": 60,
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-github",
"--toolsets",
"repos,issues,context,projects,gists"
],
"transportType": "stdio",
"headerToEnv": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "GITHUB_PERSONAL_ACCESS_TOKEN"
}
},
"bitrix24": {
"enabled": false,
"timeout": 60,
"command": "python3",
"args": [
"mcp-bitrix/mcp_server.py"
],
"transportType": "stdio",
"headerToEnv": {
"BITRIX_WEBHOOK_URL": "BITRIX_WEBHOOK_URL",
"BITRIX_ACCESS_TOKEN": "BITRIX_ACCESS_TOKEN"
}
},
"context7": {
"enabled": false,
"timeout": 60,
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-github"
"@upstash/context7-mcp"
],
"transportType": "stdio"
"transportType": "stdio",
"headerToEnv": {
"CONTEXT7_API_KEY": "CONTEXT7_API_KEY"
}
}
}
}
}
10 changes: 6 additions & 4 deletions src/mcp_proxy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from mcp.client.stdio import StdioServerParameters

from .config_loader import load_named_server_configs_from_file
from .mcp_server import MCPServerSettings, run_mcp_server
from .mcp_server import MCPServerSettings, run_mcp_server_with_dynamic_tokens
from .sse_client import run_sse_client
from .streamablehttp_client import run_streamablehttp_client

Expand Down Expand Up @@ -278,7 +278,7 @@ def _load_named_servers_from_config(
config_path: str,
base_env: dict[str, str],
logger: logging.Logger,
) -> dict[str, StdioServerParameters]:
) -> tuple[dict[str, StdioServerParameters], dict[str, dict[str, str]]]:
"""Load named server configurations from a file."""
try:
return load_named_server_configs_from_file(config_path, base_env)
Expand Down Expand Up @@ -388,12 +388,13 @@ def main() -> None:

# Configure named servers
named_stdio_params: dict[str, StdioServerParameters] = {}
header_mappings: dict[str, dict[str, str]] = {}
if args_parsed.named_server_config:
if args_parsed.named_server_definitions:
logger.warning(
"--named-server CLI arguments are ignored when --named-server-config is provided.",
)
named_stdio_params = _load_named_servers_from_config(
named_stdio_params, header_mappings = _load_named_servers_from_config(
args_parsed.named_server_config,
base_env,
logger,
Expand All @@ -416,9 +417,10 @@ def main() -> None:
# Create MCP server settings and run the server
mcp_settings = _create_mcp_settings(args_parsed)
asyncio.run(
run_mcp_server(
run_mcp_server_with_dynamic_tokens(
default_server_params=default_stdio_params,
named_server_params=named_stdio_params,
header_mappings=header_mappings,
mcp_settings=mcp_settings,
),
)
Expand Down
23 changes: 19 additions & 4 deletions src/mcp_proxy/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,25 @@
def load_named_server_configs_from_file(
config_file_path: str | Path,
base_env: dict[str, str],
) -> dict[str, StdioServerParameters]:
) -> tuple[dict[str, StdioServerParameters], dict[str, dict[str, str]]]:
"""Loads named server configurations from a JSON file.

Args:
config_file_path: Path to the JSON configuration file.
base_env: The base environment dictionary to be inherited by servers.

Returns:
A dictionary of named server parameters.
A tuple containing:
- A dictionary of named server parameters
- A dictionary of header-to-environment mappings for each server

Raises:
FileNotFoundError: If the config file is not found.
json.JSONDecodeError: If the config file contains invalid JSON.
ValueError: If the config file format is invalid.
"""
named_stdio_params: dict[str, StdioServerParameters] = {}
header_mappings: dict[str, dict[str, str]] = {}
logger.info("Loading named server configurations from: %s", config_file_path)

try:
Expand Down Expand Up @@ -70,6 +73,7 @@ def load_named_server_configs_from_file(
command = server_config.get("command")
command_args = server_config.get("args", [])
env = server_config.get("env", {})
header_to_env = server_config.get("headerToEnv", {})

if not command:
logger.warning(
Expand All @@ -83,6 +87,12 @@ def load_named_server_configs_from_file(
name,
)
continue
if not isinstance(header_to_env, dict):
logger.warning(
"Named server '%s' from config has invalid 'headerToEnv' (must be a dict). Skipping.",
name,
)
continue

new_env = base_env.copy()
new_env.update(env)
Expand All @@ -93,11 +103,16 @@ def load_named_server_configs_from_file(
env=new_env,
cwd=None,
)

# Store header mapping for this server
header_mappings[name] = header_to_env

logger.info(
"Configured named server '%s' from config: %s %s",
"Configured named server '%s' from config: %s %s (header mappings: %s)",
name,
command,
" ".join(command_args),
list(header_to_env.keys()) if header_to_env else "none",
)

return named_stdio_params
return named_stdio_params, header_mappings
Loading
Loading