fastmcp-remote 3.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ """Python stdio bridge for remote MCP servers."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("fastmcp-remote")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["__version__"]
fastmcp_remote/cli.py ADDED
@@ -0,0 +1,253 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import fnmatch
5
+ import hashlib
6
+ import os
7
+ import re
8
+ from collections.abc import Sequence
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Literal
12
+ from urllib.parse import urlparse
13
+
14
+ import anyio
15
+ from key_value.aio.protocols import AsyncKeyValue
16
+ from key_value.aio.stores.filetree import (
17
+ FileTreeStore,
18
+ FileTreeV1CollectionSanitizationStrategy,
19
+ FileTreeV1KeySanitizationStrategy,
20
+ )
21
+
22
+ from fastmcp import Client
23
+ from fastmcp.client.auth import OAuth
24
+ from fastmcp.client.transports import SSETransport, StreamableHttpTransport
25
+ from fastmcp.server import create_proxy
26
+ from fastmcp.server.transforms import GetToolNext, Transform
27
+ from fastmcp.tools import Tool
28
+ from fastmcp.utilities.versions import VersionSpec
29
+
30
+ RemoteTransport = Literal["http", "sse"]
31
+ AuthMode = Literal["oauth", "none"]
32
+ ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class RemoteConfig:
37
+ url: str
38
+ headers: dict[str, str]
39
+ transport: RemoteTransport
40
+ auth: AuthMode | None
41
+ callback_port: int | None
42
+ callback_host: str
43
+ callback_timeout: float
44
+ storage_dir: Path
45
+ ignore_tools: tuple[str, ...]
46
+ show_banner: bool
47
+ log_level: str | None
48
+
49
+
50
+ class IgnoreTools(Transform):
51
+ def __init__(self, patterns: Sequence[str]) -> None:
52
+ self.patterns = tuple(patterns)
53
+
54
+ def _matches(self, name: str) -> bool:
55
+ return any(fnmatch.fnmatchcase(name, pattern) for pattern in self.patterns)
56
+
57
+ async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
58
+ return [tool for tool in tools if not self._matches(tool.name)]
59
+
60
+ async def get_tool(
61
+ self,
62
+ name: str,
63
+ call_next: GetToolNext,
64
+ *,
65
+ version: VersionSpec | None = None,
66
+ ) -> Tool | None:
67
+ if self._matches(name):
68
+ return None
69
+ return await call_next(name, version=version)
70
+
71
+
72
+ def parse_header(value: str) -> tuple[str, str]:
73
+ name, separator, header_value = value.partition(":")
74
+ if not separator or not name.strip():
75
+ raise argparse.ArgumentTypeError("Headers must use the format 'Name: Value'.")
76
+ try:
77
+ expanded_value = ENV_VAR_PATTERN.sub(
78
+ lambda match: os.environ[match.group(1)], header_value
79
+ )
80
+ except KeyError as exc:
81
+ raise argparse.ArgumentTypeError(
82
+ f"Environment variable {exc.args[0]} is not set."
83
+ ) from exc
84
+ return name.strip(), expanded_value.strip()
85
+
86
+
87
+ def default_storage_dir(resource: str | None = None) -> Path:
88
+ if config_dir := os.environ.get("FASTMCP_REMOTE_CONFIG_DIR"):
89
+ base = Path(config_dir).expanduser()
90
+ else:
91
+ base = Path.home() / ".fastmcp" / "remote"
92
+ if resource is None:
93
+ return base
94
+ digest = hashlib.sha256(resource.encode()).hexdigest()[:16]
95
+ return base / "resources" / digest
96
+
97
+
98
+ def build_parser() -> argparse.ArgumentParser:
99
+ parser = argparse.ArgumentParser(
100
+ prog="fastmcp-remote",
101
+ description="Bridge a remote MCP server to a local stdio MCP process.",
102
+ )
103
+ parser.add_argument("url", help="Remote MCP server URL.")
104
+ parser.add_argument(
105
+ "callback_port",
106
+ nargs="?",
107
+ type=int,
108
+ help="OAuth callback port. Defaults to an available local port.",
109
+ )
110
+ parser.add_argument(
111
+ "--transport",
112
+ choices=["http", "sse"],
113
+ default="http",
114
+ help="Remote transport. Defaults to http.",
115
+ )
116
+ parser.add_argument(
117
+ "--header",
118
+ action="append",
119
+ default=[],
120
+ type=parse_header,
121
+ help="Header to send upstream, in 'Name: Value' form. Repeat for multiple headers.",
122
+ )
123
+ parser.add_argument(
124
+ "--auth",
125
+ choices=["oauth", "none"],
126
+ default=None,
127
+ help="Authentication mode. Defaults to OAuth unless Authorization is provided.",
128
+ )
129
+ parser.add_argument(
130
+ "--resource",
131
+ help="Resource identifier used to isolate OAuth token storage.",
132
+ )
133
+ parser.add_argument(
134
+ "--host",
135
+ default="localhost",
136
+ help="OAuth callback hostname. Defaults to localhost.",
137
+ )
138
+ parser.add_argument(
139
+ "--auth-timeout",
140
+ type=float,
141
+ default=300.0,
142
+ help="Seconds to wait for the OAuth callback. Defaults to 300.",
143
+ )
144
+ parser.add_argument(
145
+ "--ignore-tool",
146
+ action="append",
147
+ default=[],
148
+ help="Hide tools matching this glob pattern. Repeat for multiple patterns.",
149
+ )
150
+ parser.add_argument(
151
+ "--debug",
152
+ action="store_true",
153
+ help="Enable debug logging.",
154
+ )
155
+ parser.add_argument(
156
+ "--silent",
157
+ action="store_true",
158
+ help="Suppress non-critical logs.",
159
+ )
160
+ return parser
161
+
162
+
163
+ def parse_args(argv: Sequence[str] | None = None) -> RemoteConfig:
164
+ parser = build_parser()
165
+ args = parser.parse_args(argv)
166
+
167
+ parsed_url = urlparse(args.url)
168
+ if parsed_url.scheme not in {"http", "https"}:
169
+ parser.error("The remote MCP server URL must start with http:// or https://.")
170
+
171
+ headers = dict(args.header)
172
+ if args.silent and args.debug:
173
+ parser.error("--silent and --debug cannot be used together.")
174
+ if args.auth_timeout <= 0:
175
+ parser.error("--auth-timeout must be greater than 0.")
176
+
177
+ log_level = "DEBUG" if args.debug else None
178
+ if args.silent:
179
+ log_level = "CRITICAL"
180
+
181
+ return RemoteConfig(
182
+ url=args.url,
183
+ headers=headers,
184
+ transport=args.transport,
185
+ auth=args.auth,
186
+ callback_port=args.callback_port,
187
+ callback_host=args.host,
188
+ callback_timeout=args.auth_timeout,
189
+ storage_dir=default_storage_dir(args.resource),
190
+ ignore_tools=tuple(args.ignore_tool),
191
+ show_banner=not args.silent,
192
+ log_level=log_level,
193
+ )
194
+
195
+
196
+ def build_token_storage(storage_dir: Path) -> AsyncKeyValue:
197
+ storage_dir.mkdir(parents=True, exist_ok=True)
198
+ return FileTreeStore(
199
+ data_directory=storage_dir,
200
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(storage_dir),
201
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(
202
+ storage_dir
203
+ ),
204
+ )
205
+
206
+
207
+ def resolve_auth(config: RemoteConfig) -> OAuth | None:
208
+ authorization_header = any(
209
+ name.lower() == "authorization" for name in config.headers
210
+ )
211
+ auth_mode = config.auth
212
+ if auth_mode is None and authorization_header:
213
+ auth_mode = "none"
214
+ elif auth_mode is None:
215
+ auth_mode = "oauth"
216
+
217
+ if auth_mode == "none":
218
+ return None
219
+
220
+ return OAuth(
221
+ token_storage=build_token_storage(config.storage_dir),
222
+ callback_port=config.callback_port,
223
+ callback_host=config.callback_host,
224
+ callback_timeout=config.callback_timeout,
225
+ )
226
+
227
+
228
+ def build_transport(config: RemoteConfig) -> SSETransport | StreamableHttpTransport:
229
+ auth = resolve_auth(config)
230
+ if config.transport == "sse":
231
+ return SSETransport(config.url, headers=config.headers, auth=auth)
232
+ return StreamableHttpTransport(config.url, headers=config.headers, auth=auth)
233
+
234
+
235
+ async def run(config: RemoteConfig) -> None:
236
+ client = Client(build_transport(config))
237
+ server = create_proxy(
238
+ client,
239
+ name="fastmcp-remote",
240
+ provider_error_strategy="raise",
241
+ )
242
+ if config.ignore_tools:
243
+ server.add_transform(IgnoreTools(config.ignore_tools))
244
+ await server.run_async(
245
+ transport="stdio",
246
+ show_banner=config.show_banner,
247
+ log_level=config.log_level,
248
+ )
249
+
250
+
251
+ def main(argv: Sequence[str] | None = None) -> None:
252
+ config = parse_args(argv)
253
+ anyio.run(run, config)
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastmcp-remote
3
+ Version: 3.4.0
4
+ Summary: A Python stdio bridge for remote MCP servers, powered by FastMCP.
5
+ Project-URL: Homepage, https://gofastmcp.com
6
+ Project-URL: Repository, https://github.com/PrefectHQ/fastmcp
7
+ Project-URL: Documentation, https://gofastmcp.com
8
+ Project-URL: Original npm project, https://github.com/geelen/mcp-remote
9
+ Author: Jeremiah Lowin
10
+ License-Expression: Apache-2.0
11
+ Keywords: fastmcp,fastmcp remote,mcp,mcp remote,model context protocol,oauth,stdio
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: fastmcp-slim[client,server]==3.4.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # fastmcp-remote
25
+
26
+ `fastmcp-remote` is FastMCP's standalone Python stdio bridge for remote MCP servers. It lets MCP clients that launch local stdio processes connect to MCP servers hosted over Streamable HTTP or SSE.
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "linear": {
32
+ "command": "uvx",
33
+ "args": ["fastmcp-remote", "https://mcp.linear.app/mcp"]
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ The CLI is powered by [FastMCP](https://gofastmcp.com). Its command shape is inspired by the original [`mcp-remote`](https://github.com/geelen/mcp-remote) npm project, which established the stdio-to-remote bridge pattern used across the MCP ecosystem.
40
+
41
+ `fastmcp-remote` is intentionally smaller than the general FastMCP CLI. It does not load Python files, discover local MCP configs, prepare project environments, or run development reload loops. It builds one FastMCP client for the URL you provide, exposes that client as a local stdio proxy, and leaves the rest alone.
42
+
43
+ ## Usage
44
+
45
+ Run a remote MCP server through a local stdio bridge:
46
+
47
+ ```bash
48
+ uvx fastmcp-remote https://example.com/mcp
49
+ ```
50
+
51
+ Use the full MCP endpoint URL for the remote server. Many FastMCP HTTP servers expose MCP at `/mcp`, so a local development server may need `http://localhost:8000/mcp` rather than `http://localhost:8000`.
52
+
53
+ `fastmcp-remote` starts a local stdio bridge, then connects to the upstream server when the MCP host initializes that bridge. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or authentication cannot complete, initialization fails and the host should report the remote server as failed.
54
+
55
+ For authenticated MCP servers, OAuth is enabled automatically. To pass a bearer token or other custom header instead, provide a header. The header name ends at the first colon, so values can contain additional colons. Quote the header when the value contains spaces, just like any other shell argument:
56
+
57
+ ```bash
58
+ uvx fastmcp-remote https://example.com/mcp \
59
+ --header "Authorization: Bearer <token>"
60
+ ```
61
+
62
+ Repeat `--header` to send multiple headers. Header values use `Name: Value` format:
63
+
64
+ ```bash
65
+ uvx fastmcp-remote https://example.com/mcp \
66
+ --header "Authorization: Bearer <token>" \
67
+ --header "X-Workspace: production" \
68
+ --header "X-Client-Name: My MCP Host" \
69
+ --header "X-Callback-Url: https://example.com/oauth/callback"
70
+ ```
71
+
72
+ Some MCP hosts on Windows have trouble preserving spaces inside command arguments. Put the spaced value in an environment variable and reference it from the header value:
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "remote-api": {
78
+ "command": "uvx",
79
+ "args": [
80
+ "fastmcp-remote",
81
+ "https://example.com/mcp",
82
+ "--header",
83
+ "Authorization:${AUTH_HEADER}"
84
+ ],
85
+ "env": {
86
+ "AUTH_HEADER": "Bearer <token>"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Use `--auth none` for unauthenticated development servers:
94
+
95
+ ```bash
96
+ uvx fastmcp-remote http://localhost:8000/mcp --auth none
97
+ ```
98
+
99
+ ## Options
100
+
101
+ - `--transport`: Choose `http` or `sse`. Defaults to `http`.
102
+ - `--header`: Add a header to upstream requests, for example `--header "Authorization: Bearer <token>"`. Values may contain colons. Quote headers whose values contain spaces. Use `${VAR}` to expand environment variables inside values. Repeat for multiple headers.
103
+ - `--resource`: Isolate OAuth token storage for a named remote resource.
104
+ - `--host`: Set the OAuth callback hostname. Defaults to `localhost`.
105
+ - `--auth-timeout`: Set how long to wait for the OAuth callback. Defaults to 300 seconds.
106
+ - `--ignore-tool`: Hide tools whose names match a glob pattern.
107
+ - `--auth`: Choose `oauth` or `none`. The default uses OAuth unless an `Authorization` header is provided.
108
+
109
+ OAuth tokens are stored under `~/.fastmcp/remote` by default. Set `FASTMCP_REMOTE_CONFIG_DIR` to use another directory.
@@ -0,0 +1,7 @@
1
+ fastmcp_remote/__init__.py,sha256=gmbHvrxek2lis8GC7k0DDmmuVqfWHWEEaJP7aOqmNJQ,244
2
+ fastmcp_remote/cli.py,sha256=wJSbkMXVTSH4c_FLaMDfHUvAPt3Kv0y9sBWdVoE-H1Y,7702
3
+ fastmcp_remote/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
+ fastmcp_remote-3.4.0.dist-info/METADATA,sha256=xPamb09fBAIjjT10bB7cB9w07NkQMXp1LSb03LbnTrM,4809
5
+ fastmcp_remote-3.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ fastmcp_remote-3.4.0.dist-info/entry_points.txt,sha256=NIssuRdKqlE6UfVjOZ4aTVqeDHxueEDz2gGSCDMr0kM,59
7
+ fastmcp_remote-3.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fastmcp-remote = fastmcp_remote.cli:main