Skip to content

Commit be8d746

Browse files
authored
Merge pull request #27 from bearslyricattack/proxy
add mcp proxy.
2 parents 11a49e1 + 6569b20 commit be8d746

File tree

9 files changed

+504
-0
lines changed

9 files changed

+504
-0
lines changed

Service/mcp/mcp-proxy/Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM ghcr.io/labring-actions/devbox/debian-ssh-12.6:547a61
2+
3+
RUN cd /home/devbox/project && \rm -rf ./*
4+
COPY /Service/mcp/mcp-proxy/project /home/devbox/project/project
5+
6+
7+
RUN apt-get update && \
8+
apt-get install -y python3 python3-pip python3-venv && \
9+
apt-get clean && \
10+
rm -rf /var/lib/apt/lists/* && \
11+
ln -s /usr/bin/python3 /usr/bin/python && \
12+
chown -R devbox:devbox /home/devbox/project && \
13+
chmod -R u+rw /home/devbox/project && \
14+
chmod -R +x /home/devbox/project/project/entrypoint.sh
15+
16+
USER devbox
17+
RUN cd /home/devbox/project && \
18+
python3 -m venv .venv && \
19+
. .venv/bin/activate && \
20+
pip3 install "mcp[cli]==1.6.0" && \
21+
deactivate
22+
23+
RUN mkdir -p /home/devbox/.devbox
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# MCP Proxy Server Example
2+
3+
This is an MCP format conversion tool that can expose STDIO services in the form of SSE, or vice versa.
4+
5+
## Project Description
6+
7+
This project is a tool used to convert MCP services at the STDIO level into SSE services, so that existing MCP STDIO programs can be developed and called on the public network.
8+
9+
When you use this template for development, you need to write your own MCP service program and related dependencies in this template, After you have completed writing your code, modify the startup command in the entrypoint file by adding the stdio startup command for your project. For example:
10+
11+
```
12+
mcp-proxy --sse-port=8080 npx @modelcontextprotocol/server-puppeteer
13+
```
14+
15+
Then start the project.
16+
17+
The source code for this project comes from https://github.com/sparfenyuk/mcp-proxy.
18+
19+
## Environment
20+
21+
This project runs on a Debian 12 system with Python 3.3.2 installed. The environment is pre-configured in Devbox, so you don't need to worry about setting up Python or other system dependencies yourself. If you need to make adjustments to meet your specific requirements, you can modify the configuration file accordingly.
22+
23+
## Project Execution
24+
25+
**Development Mode**: For a normal development environment, just enter Devbox and run "bash entrypoint.sh" in the terminal. This will compile and run the Java application.
26+
**Production mode:** Once published, the project will be automatically packaged into a Docker image and deployed according to the `entrypoint.sh` script (run `bash entrypoint.sh production`).
27+
DevBox: Code. Build. Deploy. We do the rest.
28+
With DevBox, you can fully focus on writing great code while we take care of infrastructure, scaling, and deployment. Seamless development from start to production.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Library for proxying MCP servers across different transports."""
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""The entry point for the mcp-proxy application. It sets up the logging and runs the main function.
2+
3+
Two ways to run the application:
4+
1. Run the application as a module `uv run -m mcp_proxy`
5+
2. Run the application as a package `uv run mcp-proxy`
6+
7+
"""
8+
9+
import argparse
10+
import asyncio
11+
import logging
12+
import os
13+
import sys
14+
import typing as t
15+
16+
from mcp.client.stdio import StdioServerParameters
17+
18+
from .sse_client import run_sse_client
19+
from .sse_server import SseServerSettings, run_sse_server
20+
21+
SSE_URL: t.Final[str | None] = os.getenv(
22+
"SSE_URL",
23+
None,
24+
)
25+
26+
27+
def main() -> None:
28+
"""Start the client using asyncio."""
29+
parser = argparse.ArgumentParser(
30+
description=(
31+
"Start the MCP proxy in one of two possible modes: as an SSE or stdio client."
32+
),
33+
epilog=(
34+
"Examples:\n"
35+
" mcp-proxy http://localhost:8080/sse\n"
36+
" mcp-proxy --headers Authorization 'Bearer YOUR_TOKEN' http://localhost:8080/sse\n"
37+
" mcp-proxy --sse-port 8080 -- your-command --arg1 value1 --arg2 value2\n"
38+
" mcp-proxy your-command --sse-port 8080 -e KEY VALUE -e ANOTHER_KEY ANOTHER_VALUE\n"
39+
" mcp-proxy your-command --sse-port 8080 --allow-origin='*'\n"
40+
),
41+
formatter_class=argparse.RawTextHelpFormatter,
42+
)
43+
parser.add_argument(
44+
"command_or_url",
45+
help=(
46+
"Command or URL to connect to. When a URL, will run an SSE client, "
47+
"otherwise will run the given command and connect as a stdio client. "
48+
"See corresponding options for more details."
49+
),
50+
nargs="?", # Required below to allow for coming form env var
51+
default=SSE_URL,
52+
)
53+
54+
sse_client_group = parser.add_argument_group("SSE client options")
55+
sse_client_group.add_argument(
56+
"-H",
57+
"--headers",
58+
nargs=2,
59+
action="append",
60+
metavar=("KEY", "VALUE"),
61+
help="Headers to pass to the SSE server. Can be used multiple times.",
62+
default=[],
63+
)
64+
65+
stdio_client_options = parser.add_argument_group("stdio client options")
66+
stdio_client_options.add_argument(
67+
"args",
68+
nargs="*",
69+
help="Any extra arguments to the command to spawn the server",
70+
)
71+
stdio_client_options.add_argument(
72+
"-e",
73+
"--env",
74+
nargs=2,
75+
action="append",
76+
metavar=("KEY", "VALUE"),
77+
help="Environment variables used when spawning the server. Can be used multiple times.",
78+
default=[],
79+
)
80+
stdio_client_options.add_argument(
81+
"--pass-environment",
82+
action=argparse.BooleanOptionalAction,
83+
help="Pass through all environment variables when spawning the server.",
84+
default=False,
85+
)
86+
stdio_client_options.add_argument(
87+
"--debug",
88+
action=argparse.BooleanOptionalAction,
89+
help="Enable debug mode with detailed logging output.",
90+
default=False,
91+
)
92+
93+
sse_server_group = parser.add_argument_group("SSE server options")
94+
sse_server_group.add_argument(
95+
"--sse-port",
96+
type=int,
97+
default=0,
98+
help="Port to expose an SSE server on. Default is a random port",
99+
)
100+
sse_server_group.add_argument(
101+
"--sse-host",
102+
default="127.0.0.1",
103+
help="Host to expose an SSE server on. Default is 127.0.0.1",
104+
)
105+
sse_server_group.add_argument(
106+
"--allow-origin",
107+
nargs="+",
108+
default=[],
109+
help="Allowed origins for the SSE server. Can be used multiple times. Default is no CORS allowed.", # noqa: E501
110+
)
111+
112+
args = parser.parse_args()
113+
114+
if not args.command_or_url:
115+
parser.print_help()
116+
sys.exit(1)
117+
118+
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
119+
logger = logging.getLogger(__name__)
120+
121+
if (
122+
SSE_URL
123+
or args.command_or_url.startswith("http://")
124+
or args.command_or_url.startswith("https://")
125+
):
126+
# Start a client connected to the SSE server, and expose as a stdio server
127+
logger.debug("Starting SSE client and stdio server")
128+
headers = dict(args.headers)
129+
if api_access_token := os.getenv("API_ACCESS_TOKEN", None):
130+
headers["Authorization"] = f"Bearer {api_access_token}"
131+
asyncio.run(run_sse_client(args.command_or_url, headers=headers))
132+
return
133+
134+
# Start a client connected to the given command, and expose as an SSE server
135+
logger.debug("Starting stdio client and SSE server")
136+
137+
# The environment variables passed to the server process
138+
env: dict[str, str] = {}
139+
# Pass through current environment variables if configured
140+
if args.pass_environment:
141+
env.update(os.environ)
142+
# Pass in and override any environment variables with those passed on the command line
143+
env.update(dict(args.env))
144+
145+
stdio_params = StdioServerParameters(
146+
command=args.command_or_url,
147+
args=args.args,
148+
env=env,
149+
)
150+
sse_settings = SseServerSettings(
151+
bind_host=args.sse_host,
152+
port=args.sse_port,
153+
allow_origins=args.allow_origin if len(args.allow_origin) > 0 else None,
154+
log_level="DEBUG" if args.debug else "INFO",
155+
)
156+
asyncio.run(run_sse_server(stdio_params, sse_settings))
157+
158+
159+
if __name__ == "__main__":
160+
main()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
app_env=${1:-development}
4+
5+
source /home/devbox/project/.venv/bin/activate
6+
7+
export PYTHONPATH=$PYTHONPATH:/home/devbox/project
8+
9+
dev_commands() {
10+
echo "Running development environment commands..."
11+
cd /home/devbox/project && python -m project
12+
}
13+
14+
prod_commands() {
15+
echo "Running production environment commands..."
16+
cd /home/devbox/project && python -m project
17+
}
18+
19+
if [ "$app_env" = "production" ] || [ "$app_env" = "prod" ]; then
20+
echo "Production environment detected"
21+
prod_commands
22+
else
23+
echo "Development environment detected"
24+
dev_commands
25+
fi
26+
27+
deactivate
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""Create an MCP server that proxies requests throgh an MCP client.
2+
3+
This server is created independent of any transport mechanism.
4+
"""
5+
6+
import logging
7+
import typing as t
8+
9+
from mcp import server, types
10+
from mcp.client.session import ClientSession
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
async def create_proxy_server(remote_app: ClientSession) -> server.Server[object]: # noqa: C901, PLR0915
16+
"""Create a server instance from a remote app."""
17+
logger.debug("Sending initalization request to remote MCP server...")
18+
response = await remote_app.initialize()
19+
capabilities = response.capabilities
20+
21+
logger.debug("Configuring proxied MCP server...")
22+
app: server.Server[object] = server.Server(name=response.serverInfo.name)
23+
24+
if capabilities.prompts:
25+
logger.debug("Capabilities: adding Prompts...")
26+
27+
async def _list_prompts(_: t.Any) -> types.ServerResult: # noqa: ANN401
28+
result = await remote_app.list_prompts()
29+
return types.ServerResult(result)
30+
31+
app.request_handlers[types.ListPromptsRequest] = _list_prompts
32+
33+
async def _get_prompt(req: types.GetPromptRequest) -> types.ServerResult:
34+
result = await remote_app.get_prompt(req.params.name, req.params.arguments)
35+
return types.ServerResult(result)
36+
37+
app.request_handlers[types.GetPromptRequest] = _get_prompt
38+
39+
if capabilities.resources:
40+
logger.debug("Capabilities: adding Resources...")
41+
42+
async def _list_resources(_: t.Any) -> types.ServerResult: # noqa: ANN401
43+
result = await remote_app.list_resources()
44+
return types.ServerResult(result)
45+
46+
app.request_handlers[types.ListResourcesRequest] = _list_resources
47+
48+
async def _list_resource_templates(_: t.Any) -> types.ServerResult: # noqa: ANN401
49+
result = await remote_app.list_resource_templates()
50+
return types.ServerResult(result)
51+
52+
app.request_handlers[types.ListResourceTemplatesRequest] = _list_resource_templates
53+
54+
async def _read_resource(req: types.ReadResourceRequest) -> types.ServerResult:
55+
result = await remote_app.read_resource(req.params.uri)
56+
return types.ServerResult(result)
57+
58+
app.request_handlers[types.ReadResourceRequest] = _read_resource
59+
60+
if capabilities.logging:
61+
logger.debug("Capabilities: adding Logging...")
62+
63+
async def _set_logging_level(req: types.SetLevelRequest) -> types.ServerResult:
64+
await remote_app.set_logging_level(req.params.level)
65+
return types.ServerResult(types.EmptyResult())
66+
67+
app.request_handlers[types.SetLevelRequest] = _set_logging_level
68+
69+
if capabilities.resources:
70+
logger.debug("Capabilities: adding Resources...")
71+
72+
async def _subscribe_resource(req: types.SubscribeRequest) -> types.ServerResult:
73+
await remote_app.subscribe_resource(req.params.uri)
74+
return types.ServerResult(types.EmptyResult())
75+
76+
app.request_handlers[types.SubscribeRequest] = _subscribe_resource
77+
78+
async def _unsubscribe_resource(req: types.UnsubscribeRequest) -> types.ServerResult:
79+
await remote_app.unsubscribe_resource(req.params.uri)
80+
return types.ServerResult(types.EmptyResult())
81+
82+
app.request_handlers[types.UnsubscribeRequest] = _unsubscribe_resource
83+
84+
if capabilities.tools:
85+
logger.debug("Capabilities: adding Tools...")
86+
87+
async def _list_tools(_: t.Any) -> types.ServerResult: # noqa: ANN401
88+
tools = await remote_app.list_tools()
89+
return types.ServerResult(tools)
90+
91+
app.request_handlers[types.ListToolsRequest] = _list_tools
92+
93+
async def _call_tool(req: types.CallToolRequest) -> types.ServerResult:
94+
try:
95+
result = await remote_app.call_tool(
96+
req.params.name,
97+
(req.params.arguments or {}),
98+
)
99+
return types.ServerResult(result)
100+
except Exception as e: # noqa: BLE001
101+
return types.ServerResult(
102+
types.CallToolResult(
103+
content=[types.TextContent(type="text", text=str(e))],
104+
isError=True,
105+
),
106+
)
107+
108+
app.request_handlers[types.CallToolRequest] = _call_tool
109+
110+
async def _send_progress_notification(req: types.ProgressNotification) -> None:
111+
await remote_app.send_progress_notification(
112+
req.params.progressToken,
113+
req.params.progress,
114+
req.params.total,
115+
)
116+
117+
app.notification_handlers[types.ProgressNotification] = _send_progress_notification
118+
119+
async def _complete(req: types.CompleteRequest) -> types.ServerResult:
120+
result = await remote_app.complete(
121+
req.params.ref,
122+
req.params.argument.model_dump(),
123+
)
124+
return types.ServerResult(result)
125+
126+
app.request_handlers[types.CompleteRequest] = _complete
127+
128+
return app

Service/mcp/mcp-proxy/project/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)