From 2d214b68edf5cfc64c554d2ae31bd6e5d8712874 Mon Sep 17 00:00:00 2001 From: Travis Vasceannie Date: Wed, 26 Nov 2025 08:32:24 -0500 Subject: [PATCH] x --- Dockerfile | 3 + build-and-push.sh | 50 ++++++++++++ config.json | 56 ++++++++++++++ config_example.json | 23 ------ docker-compose.yml | 147 ++++++++++++++++++++++++++++++++++++ src/mcp_proxy/mcp_server.py | 20 ++++- 6 files changed, 275 insertions(+), 24 deletions(-) create mode 100755 build-and-push.sh create mode 100644 config.json delete mode 100644 config_example.json create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile index b9b5e9e..4ee0618 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,9 @@ RUN apk add --update --no-cache catatonit # Final stage with explicit platform specification FROM python:3.13-alpine +# Install Node.js and npm +RUN apk add --no-cache nodejs npm + COPY --from=uv --chown=app:app /app/.venv /app/.venv COPY --from=uv /usr/bin/catatonit /usr/bin/ COPY --from=uv /usr/libexec/podman/catatonit /usr/libexec/podman/ diff --git a/build-and-push.sh b/build-and-push.sh new file mode 100755 index 0000000..8fae3e8 --- /dev/null +++ b/build-and-push.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +REGISTRY="git.baked.rocks" +OWNER="vasceannie" +# Extract service names from docker-compose.yml +SERVICES=($(docker compose config --services)) + +# Login to registry if not already authenticated +echo "Logging in to ${REGISTRY}..." +docker login "${REGISTRY}" || { + echo "Error: Failed to login to ${REGISTRY}" + echo "Please ensure you have valid credentials" + exit 1 +} + +# Build all services +echo "Building all services..." +docker compose build + +# Tag and push each service +for service in "${SERVICES[@]}"; do + echo "Tagging and pushing $service..." + # Get the image ID - try multiple methods + IMAGE_ID=$(docker compose images -q "$service" 2>/dev/null | head -1) + + if [ -z "$IMAGE_ID" ]; then + # Fallback: find image by service name pattern + IMAGE_ID=$(docker images --format "{{.ID}}" --filter "reference=*${service}*" | head -1) + fi + + if [ -z "$IMAGE_ID" ]; then + echo "Error: Could not find image ID for $service" + echo "Available images:" + docker compose images + docker images | grep -E "(${SERVICES[*]// /|})" + exit 1 + fi + + TARGET_IMAGE="${OWNER}/${service}:latest" + REGISTRY_IMAGE="${REGISTRY}/${TARGET_IMAGE}" + + # Tag using image ID directly + docker tag "${IMAGE_ID}" "${REGISTRY_IMAGE}" + docker push "${REGISTRY_IMAGE}" + echo "$service pushed successfully" +done + +echo "All services built and pushed to ${REGISTRY}" + diff --git a/config.json b/config.json new file mode 100644 index 0000000..19071e4 --- /dev/null +++ b/config.json @@ -0,0 +1,56 @@ +{ + "mcpServers": { + "pieces": { + "enabled": true, + "timeout": 60, + "url": "http://localhost:39300/model_context_protocol/2024-11-05/sse", + "transportType": "sse", + "command": "echo", + "args": ["sse-placeholder"] + }, + "firecrawl": { + "enabled": true, + "timeout": 60, + "command": "npx", + "args": [ + "-y", + "firecrawl-mcp" + ], + "transportType": "stdio", + "env": { + "FIRECRAWL_API_URL": "http://crawl.lab:30002", + "FIRECRAWL_API_KEY": "dummy-key" + } + }, + "context7": { + "enabled": true, + "timeout": 60, + "url": "https://mcp.context7.com/mcp", + "transportType": "http", + "command": "echo", + "args": ["http-placeholder"], + "headers": { + "CONTEXT7_API_KEY": "ctx7sk-f6f1b998-88a2-4e78-9d21-433545326e6c" + } + }, + "knowledge-graph": { + "enabled": true, + "timeout": 60, + "url": "http://localhost:48000/sse", + "transportType": "sse", + "command": "echo", + "args": ["sse-placeholder"] + }, + "xpipe": { + "enabled": true, + "timeout": 60, + "url": "http://localhost:21721/mcp", + "transportType": "http", + "command": "echo", + "args": ["http-placeholder"], + "headers": { + "Authorization": "Bearer 27d1c4ca-dfec-4d38-adc1-1e0f4bcbaadc" + } + } + } +} diff --git a/config_example.json b/config_example.json deleted file mode 100644 index 760f055..0000000 --- a/config_example.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "mcpServers": { - "fetch": { - "enabled": true, - "timeout": 60, - "command": "uvx", - "args": [ - "mcp-server-fetch" - ], - "transportType": "stdio" - }, - "github": { - "enabled": false, - "timeout": 60, - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-github" - ], - "transportType": "stdio" - } - } -} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..99cc7d2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,147 @@ +version: '3.8' + +networks: + traefik: + external: true + +services: + pieces-mcp: + image: git.baked.rocks/vasceannie/pieces-mcp:latest + deploy: + placement: + constraints: + - node.hostname == wslbox + labels: + - "traefik.enable=true" + - "traefik.http.routers.pieces-mcp.rule=Host(`mcp.box`) && PathPrefix(`/pieces-mcp`)" + - "traefik.http.routers.pieces-mcp.entrypoints=web" + - "traefik.http.services.pieces-mcp.loadbalancer.server.port=8096" + - "traefik.http.middlewares.pieces-mcp-stripprefix.stripprefix.prefixes=/pieces-mcp" + - "traefik.http.routers.pieces-mcp.middlewares=pieces-mcp-stripprefix" + restart_policy: + condition: on-failure + healthcheck: + test: ["CMD", "sh", "-c", "netstat -tln | grep ':8096 ' || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + command: > + --named-server pieces-os 'mcp-proxy --transport sse http://host.docker.internal:39300/model_context_protocol/2024-11-05/sse' + --port=8096 --host=0.0.0.0 --allow-origin='*' + networks: + - traefik + + firecrawl-mcp: + image: git.baked.rocks/vasceannie/firecrawl-mcp:latest + deploy: + placement: + constraints: + - node.hostname == wslbox + labels: + - "traefik.enable=true" + - "traefik.http.routers.firecrawl-mcp.rule=Host(`mcp.box`) && PathPrefix(`/firecrawl-mcp`)" + - "traefik.http.routers.firecrawl-mcp.entrypoints=web" + - "traefik.http.services.firecrawl-mcp.loadbalancer.server.port=8097" + - "traefik.http.middlewares.firecrawl-mcp-stripprefix.stripprefix.prefixes=/firecrawl-mcp" + - "traefik.http.routers.firecrawl-mcp.middlewares=firecrawl-mcp-stripprefix" + restart_policy: + condition: on-failure + environment: + - FIRECRAWL_API_URL=http://crawl.toy + - FIRECRAWL_API_KEY=dummy-key + healthcheck: + test: ["CMD", "sh", "-c", "netstat -tln | grep ':809[6-9]\\|:8100' || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + command: > + --pass-environment + --stateless + --transport streamablehttp + --port=8097 --host=0.0.0.0 + npx firecrawl-mcp + networks: + - traefik + + context7-mcp: + image: git.baked.rocks/vasceannie/context7-mcp:latest + deploy: + placement: + constraints: + - node.hostname == wslbox + labels: + - "traefik.enable=true" + - "traefik.http.routers.context7-mcp.rule=Host(`mcp.box`) && PathPrefix(`/context7-mcp`)" + - "traefik.http.routers.context7-mcp.entrypoints=web" + - "traefik.http.services.context7-mcp.loadbalancer.server.port=8098" + - "traefik.http.middlewares.context7-mcp-stripprefix.stripprefix.prefixes=/context7-mcp" + - "traefik.http.routers.context7-mcp.middlewares=context7-mcp-stripprefix" + restart_policy: + condition: on-failure + healthcheck: + test: ["CMD", "sh", "-c", "netstat -tln | grep ':809[6-9]\\|:8100' || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + command: > + --named-server context7 'mcp-proxy --stateless --transport streamablehttp --headers CONTEXT7_API_KEY ctx7sk-f6f1b998-88a2-4e78-9d21-433545326e6c https://mcp.context7.com/mcp' + --port=8098 --host=0.0.0.0 --allow-origin='*' + networks: + - traefik + + # knowledge-graph-mcp: + # image: git.baked.rocks/vasceannie/knowledge-graph-mcp:latest + # deploy: + # placement: + # constraints: + # - node.hostname == wslbox + # labels: + # - "traefik.enable=true" + # - "traefik.http.routers.knowledge-graph-mcp.rule=Host(`mcp.box`) && PathPrefix(`/knowledge-graph-mcp`)" + # - "traefik.http.routers.knowledge-graph-mcp.entrypoints=web" + # - "traefik.http.services.knowledge-graph-mcp.loadbalancer.server.port=8099" + # - "traefik.http.middlewares.knowledge-graph-mcp-stripprefix.stripprefix.prefixes=/knowledge-graph-mcp" + # - "traefik.http.routers.knowledge-graph-mcp.middlewares=knowledge-graph-mcp-stripprefix" + # restart_policy: + # condition: on-failure + # healthcheck: + # test: ["CMD", "sh", "-c", "netstat -tln | grep ':809[6-9]\\|:8100' || exit 1"] + # interval: 30s + # timeout: 10s + # retries: 3 + # start_period: 40s + # command: > + # --named-server knowledge-graph 'mcp-proxy --transport sse http://host.docker.internal:48000/sse' + # --port=8099 --host=0.0.0.0 --allow-origin='*' + # networks: + # - traefik + + xpipe-mcp: + image: git.baked.rocks/vasceannie/xpipe-mcp:latest + deploy: + placement: + constraints: + - node.hostname == wslbox + labels: + - "traefik.enable=true" + - "traefik.http.routers.xpipe-mcp.rule=Host(`mcp.box`) && PathPrefix(`/xpipe-mcp`)" + - "traefik.http.routers.xpipe-mcp.entrypoints=web" + - "traefik.http.services.xpipe-mcp.loadbalancer.server.port=8100" + - "traefik.http.middlewares.xpipe-mcp-stripprefix.stripprefix.prefixes=/xpipe-mcp" + - "traefik.http.routers.xpipe-mcp.middlewares=xpipe-mcp-stripprefix" + restart_policy: + condition: on-failure + healthcheck: + test: ["CMD", "sh", "-c", "netstat -tln | grep ':809[6-9]\\|:8100' || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + command: > + --named-server xpipe 'mcp-proxy --transport streamablehttp --headers Authorization "Bearer 27d1c4ca-dfec-4d38-adc1-1e0f4bcbaadc" http://host.docker.internal:21721/mcp' + --port=8100 --host=0.0.0.0 --allow-origin='*' + networks: + - traefik \ No newline at end of file diff --git a/src/mcp_proxy/mcp_server.py b/src/mcp_proxy/mcp_server.py index a39abb5..d01d625 100644 --- a/src/mcp_proxy/mcp_server.py +++ b/src/mcp_proxy/mcp_server.py @@ -90,10 +90,28 @@ def create_single_instance_routes( _update_global_activity() await http_session_manager.handle_request(scope, receive, send) + async def handle_sse_messages(scope: Scope, receive: Receive, send: Send) -> None: + _update_global_activity() + try: + await sse_transport.handle_post_message(scope, receive, send) + except Exception as e: + # If session is not found or other SSE errors, return a proper HTTP response + if "Could not find session" in str(e) or "session" in str(e).lower(): + logger.warning("Session not found for SSE message request, client should reconnect: %s", e) + response = JSONResponse( + {"error": "Session not found", "message": "Please reconnect to establish a new session"}, + status_code=410, # 410 Gone - indicates resource is no longer available + ) + await response(scope, receive, send) + else: + # Re-raise other exceptions + raise + routes = [ Mount("/mcp", app=handle_streamable_http_instance), Route("/sse", endpoint=handle_sse_instance), - Mount("/messages/", app=sse_transport.handle_post_message), + Mount("/messages/", app=handle_sse_messages), + Mount("/", app=handle_streamable_http_instance), ] return routes, http_session_manager