harvesthealth commited on
Commit
79a3949
·
verified ·
1 Parent(s): 4a597ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -53
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
- # Test if subprocess is actually running
 
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
- logger.info("Received request body: %s", body.decode())
 
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
- response = requests.get("https://api.github.com/user", headers={"Authorization": f"token {token}"})
88
- logger.info("Token verification response status: %s", response.status_code)
89
- if response.status_code == 200:
 
90
  logger.info("GitHub token verification successful")
91
  else:
92
- logger.error("GitHub token verification failed with status code: %s", response.status_code)
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 if we have a complete JSON message
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
- # Try to parse as JSON
143
- import json
144
  try:
145
  json_data = json.loads(response_str)
146
- # Check if it's a valid JSON-RPC response
 
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
- logger.info("Full response: %s", response_str)
 
152
  break
 
153
  else:
154
- logger.debug("JSON-RPC response missing id or error field")
 
155
  except json.JSONDecodeError as e:
156
- # If JSON parsing fails, continue reading
157
- logger.debug("JSON parsing failed (expected during partial reads): %s", str(e))
158
- # Continue reading
159
  continue
160
  else:
161
- # Empty response, continue reading
162
  logger.debug("Empty response, continuing to read")
163
  continue
164
- except Exception as e:
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
- # Continue reading for more data
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
- # Log final response
181
  logger.info("Final response size: %d bytes", len(response))
182
- if len(response) > 0:
183
- try:
184
- final_response_str = bytes(response).decode("utf-8")
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