Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import asyncio
|
| 2 |
import os
|
| 3 |
import logging
|
|
|
|
|
|
|
| 4 |
from fastapi import FastAPI, Request, Response
|
| 5 |
|
| 6 |
# Set up logging with more detailed configuration
|
|
@@ -18,16 +20,17 @@ async def startup_event():
|
|
| 18 |
global proc
|
| 19 |
logger.info("=== STARTUP EVENT ===")
|
| 20 |
# Get the token from an environment variable on the Hugging Face Space
|
| 21 |
-
# This is a more secure way to handle the token
|
| 22 |
token = os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN")
|
| 23 |
logger.info("Attempting to start subprocess...")
|
| 24 |
logger.info("Token environment variable exists: %s", token is not None)
|
|
|
|
| 25 |
if not token:
|
| 26 |
logger.error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable not set")
|
| 27 |
return
|
| 28 |
|
| 29 |
logger.info("Starting subprocess with token: %s", token[:10] + "...")
|
| 30 |
try:
|
|
|
|
| 31 |
proc = await asyncio.create_subprocess_exec(
|
| 32 |
'/usr/local/bin/github-mcp-server', 'stdio',
|
| 33 |
stdin=asyncio.subprocess.PIPE,
|
|
@@ -36,26 +39,29 @@ async def startup_event():
|
|
| 36 |
env={"GITHUB_PERSONAL_ACCESS_TOKEN": token}
|
| 37 |
)
|
| 38 |
logger.info("Subprocess created successfully with PID: %s", proc.pid)
|
| 39 |
-
|
|
|
|
| 40 |
if proc.returncode is None:
|
| 41 |
logger.info("Subprocess appears to be running")
|
| 42 |
else:
|
| 43 |
logger.warning("Subprocess may have exited immediately with code: %s", proc.returncode)
|
|
|
|
|
|
|
| 44 |
asyncio.create_task(log_stderr())
|
| 45 |
logger.info("=== STARTUP COMPLETE ===")
|
| 46 |
except Exception as e:
|
| 47 |
logger.error("Failed to create subprocess: %s", str(e))
|
|
|
|
| 48 |
raise
|
| 49 |
|
| 50 |
async def log_stderr():
|
|
|
|
| 51 |
if proc and proc.stderr:
|
| 52 |
logger.info("Starting stderr logging task")
|
| 53 |
-
while
|
| 54 |
-
|
| 55 |
-
<< 35 Characters hidden >>
|
| 56 |
-
|
| 57 |
-
try:
|
| 58 |
line = await proc.stderr.readline()
|
|
|
|
| 59 |
if line:
|
| 60 |
logger.debug("github-mcp-server stderr: %s", line.decode().strip())
|
| 61 |
else:
|
|
@@ -70,50 +76,50 @@ async def proxy(request: Request):
|
|
| 70 |
logger.info("Request method: %s", request.method)
|
| 71 |
logger.info("Request headers: %s", dict(request.headers))
|
| 72 |
|
|
|
|
| 73 |
if not proc or not proc.stdin or not proc.stdout:
|
| 74 |
logger.error("Subprocess not running - returning 500")
|
| 75 |
return Response(status_code=500, content="Subprocess not running")
|
| 76 |
|
| 77 |
-
# Log incoming request
|
| 78 |
body = await request.body()
|
| 79 |
-
|
|
|
|
| 80 |
|
| 81 |
-
# Token verification for debugging
|
| 82 |
-
import requests
|
| 83 |
token = os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN")
|
| 84 |
if token:
|
| 85 |
try:
|
| 86 |
logger.info("Testing GitHub token with API call...")
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
| 90 |
logger.info("GitHub token verification successful")
|
| 91 |
else:
|
| 92 |
-
logger.error("GitHub token verification failed with status code: %s",
|
| 93 |
except Exception as e:
|
| 94 |
logger.error("Error verifying GitHub token: %s", str(e))
|
| 95 |
else:
|
| 96 |
logger.warning("No GitHub token found in environment")
|
| 97 |
|
| 98 |
-
# Send to subprocess
|
| 99 |
logger.info("Sending request to subprocess...")
|
| 100 |
try:
|
| 101 |
proc.stdin.write(body)
|
| 102 |
await proc.stdin.drain()
|
| 103 |
-
logger.info("Successfully wrote to subprocess stdin")
|
| 104 |
except Exception as e:
|
| 105 |
logger.error("Error writing to subprocess stdin: %s", str(e))
|
| 106 |
return Response(status_code=500, content=f"Error writing to subprocess: {str(e)}")
|
| 107 |
|
| 108 |
-
# Read response with timeout
|
| 109 |
logger.info("Reading response from subprocess with timeout...")
|
| 110 |
response = bytearray()
|
|
|
|
|
|
|
|
|
|
| 111 |
try:
|
| 112 |
-
# Set a reasonable timeout (e.g., 30 seconds)
|
| 113 |
-
import asyncio
|
| 114 |
-
timeout_seconds = 30
|
| 115 |
-
start_time = asyncio.get_event_loop().time()
|
| 116 |
-
|
| 117 |
while True:
|
| 118 |
elapsed = asyncio.get_event_loop().time() - start_time
|
| 119 |
if elapsed > timeout_seconds:
|
|
@@ -121,6 +127,7 @@ async def proxy(request: Request):
|
|
| 121 |
break
|
| 122 |
|
| 123 |
try:
|
|
|
|
| 124 |
chunk = await asyncio.wait_for(proc.stdout.read(1024), timeout=5.0)
|
| 125 |
logger.debug("Received chunk of size: %d", len(chunk))
|
| 126 |
|
|
@@ -131,63 +138,53 @@ async def proxy(request: Request):
|
|
| 131 |
response.extend(chunk)
|
| 132 |
logger.debug("Extended response with chunk, total size: %d", len(response))
|
| 133 |
|
| 134 |
-
# Check
|
| 135 |
try:
|
| 136 |
response_str = bytes(response).decode("utf-8")
|
| 137 |
-
logger.debug("Decoded response: %s", response_str[:200] + "..." if len(response_str) > 200 else response_str)
|
| 138 |
|
| 139 |
-
# More robust JSON-RPC detection
|
| 140 |
-
# Check if we have a complete JSON object
|
| 141 |
if len(response_str.strip()) > 0:
|
| 142 |
-
#
|
| 143 |
-
import json
|
| 144 |
try:
|
| 145 |
json_data = json.loads(response_str)
|
| 146 |
-
|
|
|
|
| 147 |
if isinstance(json_data, dict) and "jsonrpc" in json_data:
|
| 148 |
-
# Check if it's a response (has id or error)
|
| 149 |
if "id" in json_data or "error" in json_data:
|
| 150 |
logger.info("Complete JSON-RPC response detected")
|
| 151 |
-
|
|
|
|
| 152 |
break
|
|
|
|
| 153 |
else:
|
| 154 |
-
logger.debug("JSON-RPC response missing id or error
|
|
|
|
| 155 |
except json.JSONDecodeError as e:
|
| 156 |
-
#
|
| 157 |
-
logger.debug("JSON parsing failed (
|
| 158 |
-
# Continue reading
|
| 159 |
continue
|
| 160 |
else:
|
| 161 |
-
# Empty response, continue reading
|
| 162 |
logger.debug("Empty response, continuing to read")
|
| 163 |
continue
|
| 164 |
-
except
|
| 165 |
-
logger.warning("Decoding failed: %s", str(e))
|
| 166 |
-
# Continue reading
|
| 167 |
continue
|
| 168 |
except asyncio.TimeoutError:
|
| 169 |
-
logger.warning("Timeout waiting for chunk from subprocess")
|
| 170 |
-
#
|
| 171 |
continue
|
| 172 |
except Exception as e:
|
| 173 |
logger.error("Error reading chunk from subprocess: %s", str(e))
|
| 174 |
break
|
| 175 |
|
| 176 |
except Exception as e:
|
| 177 |
-
logger.error("Error in response reading loop: %s", str(e))
|
| 178 |
return Response(status_code=500, content=f"Error reading response: {str(e)}")
|
| 179 |
|
| 180 |
-
#
|
| 181 |
logger.info("Final response size: %d bytes", len(response))
|
| 182 |
-
if len(response)
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
logger.info("Final response preview: %s", final_response_str[:200] + "..." if len(final_response_str) > 200 else final_response_str)
|
| 186 |
-
except Exception as e:
|
| 187 |
-
logger.error("Error decoding final response: %s", str(e))
|
| 188 |
-
else:
|
| 189 |
-
logger.warning("Final response is empty")
|
| 190 |
-
|
| 191 |
logger.info("=== REQUEST PROCESSING COMPLETE ===")
|
| 192 |
return Response(content=bytes(response))
|
| 193 |
|
|
|
|
| 1 |
import asyncio
|
| 2 |
import os
|
| 3 |
import logging
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
from fastapi import FastAPI, Request, Response
|
| 7 |
|
| 8 |
# Set up logging with more detailed configuration
|
|
|
|
| 20 |
global proc
|
| 21 |
logger.info("=== STARTUP EVENT ===")
|
| 22 |
# Get the token from an environment variable on the Hugging Face Space
|
|
|
|
| 23 |
token = os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN")
|
| 24 |
logger.info("Attempting to start subprocess...")
|
| 25 |
logger.info("Token environment variable exists: %s", token is not None)
|
| 26 |
+
|
| 27 |
if not token:
|
| 28 |
logger.error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable not set")
|
| 29 |
return
|
| 30 |
|
| 31 |
logger.info("Starting subprocess with token: %s", token[:10] + "...")
|
| 32 |
try:
|
| 33 |
+
# Create the subprocess to run the github-mcp-server
|
| 34 |
proc = await asyncio.create_subprocess_exec(
|
| 35 |
'/usr/local/bin/github-mcp-server', 'stdio',
|
| 36 |
stdin=asyncio.subprocess.PIPE,
|
|
|
|
| 39 |
env={"GITHUB_PERSONAL_ACCESS_TOKEN": token}
|
| 40 |
)
|
| 41 |
logger.info("Subprocess created successfully with PID: %s", proc.pid)
|
| 42 |
+
|
| 43 |
+
# Check if subprocess is actually running
|
| 44 |
if proc.returncode is None:
|
| 45 |
logger.info("Subprocess appears to be running")
|
| 46 |
else:
|
| 47 |
logger.warning("Subprocess may have exited immediately with code: %s", proc.returncode)
|
| 48 |
+
|
| 49 |
+
# Start the background task to log stderr
|
| 50 |
asyncio.create_task(log_stderr())
|
| 51 |
logger.info("=== STARTUP COMPLETE ===")
|
| 52 |
except Exception as e:
|
| 53 |
logger.error("Failed to create subprocess: %s", str(e))
|
| 54 |
+
# Re-raise to crash the application startup, as the proxy cannot function without the subprocess
|
| 55 |
raise
|
| 56 |
|
| 57 |
async def log_stderr():
|
| 58 |
+
"""Reads and logs output from the subprocess's stderr stream."""
|
| 59 |
if proc and proc.stderr:
|
| 60 |
logger.info("Starting stderr logging task")
|
| 61 |
+
while not proc.stderr.at_eof():
|
| 62 |
+
try:
|
|
|
|
|
|
|
|
|
|
| 63 |
line = await proc.stderr.readline()
|
| 64 |
+
|
| 65 |
if line:
|
| 66 |
logger.debug("github-mcp-server stderr: %s", line.decode().strip())
|
| 67 |
else:
|
|
|
|
| 76 |
logger.info("Request method: %s", request.method)
|
| 77 |
logger.info("Request headers: %s", dict(request.headers))
|
| 78 |
|
| 79 |
+
# 1. Check subprocess health
|
| 80 |
if not proc or not proc.stdin or not proc.stdout:
|
| 81 |
logger.error("Subprocess not running - returning 500")
|
| 82 |
return Response(status_code=500, content="Subprocess not running")
|
| 83 |
|
| 84 |
+
# 2. Log incoming request body
|
| 85 |
body = await request.body()
|
| 86 |
+
# Decode only a small portion for logging to prevent console spam for huge payloads
|
| 87 |
+
logger.info("Received request body preview: %s", body.decode()[:200] + "..." if len(body) > 200 else body.decode())
|
| 88 |
|
| 89 |
+
# 3. Token verification for debugging (Synchronous call to external API)
|
|
|
|
| 90 |
token = os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN")
|
| 91 |
if token:
|
| 92 |
try:
|
| 93 |
logger.info("Testing GitHub token with API call...")
|
| 94 |
+
# Note: requests.get is blocking, but typically fast enough here.
|
| 95 |
+
response_gh = requests.get("https://api.github.com/user", headers={"Authorization": f"token {token}"}, timeout=5)
|
| 96 |
+
logger.info("Token verification response status: %s", response_gh.status_code)
|
| 97 |
+
if response_gh.status_code == 200:
|
| 98 |
logger.info("GitHub token verification successful")
|
| 99 |
else:
|
| 100 |
+
logger.error("GitHub token verification failed with status code: %s", response_gh.status_code)
|
| 101 |
except Exception as e:
|
| 102 |
logger.error("Error verifying GitHub token: %s", str(e))
|
| 103 |
else:
|
| 104 |
logger.warning("No GitHub token found in environment")
|
| 105 |
|
| 106 |
+
# 4. Send request to subprocess
|
| 107 |
logger.info("Sending request to subprocess...")
|
| 108 |
try:
|
| 109 |
proc.stdin.write(body)
|
| 110 |
await proc.stdin.drain()
|
| 111 |
+
logger.info("Successfully wrote request body to subprocess stdin")
|
| 112 |
except Exception as e:
|
| 113 |
logger.error("Error writing to subprocess stdin: %s", str(e))
|
| 114 |
return Response(status_code=500, content=f"Error writing to subprocess: {str(e)}")
|
| 115 |
|
| 116 |
+
# 5. Read response with timeout and robust JSON detection
|
| 117 |
logger.info("Reading response from subprocess with timeout...")
|
| 118 |
response = bytearray()
|
| 119 |
+
timeout_seconds = 30
|
| 120 |
+
start_time = asyncio.get_event_loop().time()
|
| 121 |
+
|
| 122 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
while True:
|
| 124 |
elapsed = asyncio.get_event_loop().time() - start_time
|
| 125 |
if elapsed > timeout_seconds:
|
|
|
|
| 127 |
break
|
| 128 |
|
| 129 |
try:
|
| 130 |
+
# Use wait_for to enforce a per-read timeout
|
| 131 |
chunk = await asyncio.wait_for(proc.stdout.read(1024), timeout=5.0)
|
| 132 |
logger.debug("Received chunk of size: %d", len(chunk))
|
| 133 |
|
|
|
|
| 138 |
response.extend(chunk)
|
| 139 |
logger.debug("Extended response with chunk, total size: %d", len(response))
|
| 140 |
|
| 141 |
+
# Check for complete JSON-RPC message
|
| 142 |
try:
|
| 143 |
response_str = bytes(response).decode("utf-8")
|
|
|
|
| 144 |
|
|
|
|
|
|
|
| 145 |
if len(response_str.strip()) > 0:
|
| 146 |
+
# Attempt to parse as JSON
|
|
|
|
| 147 |
try:
|
| 148 |
json_data = json.loads(response_str)
|
| 149 |
+
|
| 150 |
+
# Check for mandatory JSON-RPC fields in a response
|
| 151 |
if isinstance(json_data, dict) and "jsonrpc" in json_data:
|
|
|
|
| 152 |
if "id" in json_data or "error" in json_data:
|
| 153 |
logger.info("Complete JSON-RPC response detected")
|
| 154 |
+
# Log only a preview of the full response
|
| 155 |
+
logger.info("Full response preview: %s", response_str[:200] + "..." if len(response_str) > 200 else response_str)
|
| 156 |
break
|
| 157 |
+
# If it's a request, we keep reading (though the subprocess should only send responses)
|
| 158 |
else:
|
| 159 |
+
logger.debug("JSON-RPC object found, but not a final response (missing 'id' or 'error'). Continuing to read.")
|
| 160 |
+
|
| 161 |
except json.JSONDecodeError as e:
|
| 162 |
+
# Expected when reading partial JSON data. Continue reading.
|
| 163 |
+
logger.debug("JSON parsing failed (partial read): %s", str(e))
|
|
|
|
| 164 |
continue
|
| 165 |
else:
|
|
|
|
| 166 |
logger.debug("Empty response, continuing to read")
|
| 167 |
continue
|
| 168 |
+
except UnicodeDecodeError as e:
|
| 169 |
+
logger.warning("Decoding failed (partial multi-byte char): %s", str(e))
|
|
|
|
| 170 |
continue
|
| 171 |
except asyncio.TimeoutError:
|
| 172 |
+
logger.warning("Timeout waiting for chunk from subprocess, continuing loop.")
|
| 173 |
+
# This timeout is okay, it allows the main timeout check to fire if needed.
|
| 174 |
continue
|
| 175 |
except Exception as e:
|
| 176 |
logger.error("Error reading chunk from subprocess: %s", str(e))
|
| 177 |
break
|
| 178 |
|
| 179 |
except Exception as e:
|
| 180 |
+
logger.error("Error in primary response reading loop: %s", str(e))
|
| 181 |
return Response(status_code=500, content=f"Error reading response: {str(e)}")
|
| 182 |
|
| 183 |
+
# 6. Final response check and return
|
| 184 |
logger.info("Final response size: %d bytes", len(response))
|
| 185 |
+
if len(response) == 0:
|
| 186 |
+
logger.error("Final response is empty after reading loop.")
|
| 187 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
logger.info("=== REQUEST PROCESSING COMPLETE ===")
|
| 189 |
return Response(content=bytes(response))
|
| 190 |
|