Skip to content

Commit 0a7fd3d

Browse files
committed
fix(sse_app): Expose SSE App correctly for production setup
1 parent 85fc83a commit 0a7fd3d

File tree

14 files changed

+355
-77
lines changed

14 files changed

+355
-77
lines changed

packages/developer_mcp_server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ sentry = [
3838

3939

4040
[project.scripts]
41-
developer-mcp-server = "developer_mcp_server.server:run_mcp_server"
41+
developer-mcp-server = "developer_mcp_server.run:run_mcp_server"
4242

4343
[build-system]
4444
requires = ["hatchling"]
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Runtime entry points for the Developer MCP server.
2+
3+
This module provides different ways to run the MCP server:
4+
- stdio: Standard input/output transport (default for CLI tools)
5+
- http: HTTP/SSE transport using uvicorn (for local development)
6+
"""
7+
8+
import logging
9+
import os
10+
11+
from gg_api_core.sentry_integration import init_sentry
12+
13+
from developer_mcp_server.server import mcp
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
def run_stdio():
19+
"""Run the MCP server over stdio transport.
20+
21+
This is the default mode for MCP servers, used when the server
22+
is invoked as a subprocess by MCP clients like Claude Desktop.
23+
"""
24+
init_sentry()
25+
logger.info("Developer MCP server running on stdio")
26+
mcp.run()
27+
28+
29+
def run_http_with_uvicorn():
30+
"""Run the MCP server over HTTP using uvicorn ASGI server.
31+
32+
This is meant for local development. For production ready setup,
33+
better use gunicorn with uvicorn ASGI workers via ggmcp_http.sse_app:app
34+
"""
35+
init_sentry()
36+
37+
# Get host and port from environment variables
38+
mcp_port = int(os.environ.get("MCP_PORT", "8000"))
39+
mcp_host = os.environ.get("MCP_HOST", "127.0.0.1")
40+
41+
# Use HTTP/SSE transport with uvicorn
42+
import uvicorn
43+
44+
logger.info(f"Starting Developer MCP server on {mcp_host}:{mcp_port}")
45+
uvicorn.run(mcp.sse_app(), host=mcp_host, port=mcp_port)
46+
47+
48+
def run_mcp_server():
49+
"""Run the MCP server with transport auto-detection.
50+
51+
If MCP_PORT is set, uses HTTP/SSE transport.
52+
Otherwise, uses stdio transport (default).
53+
"""
54+
mcp_port = os.environ.get("MCP_PORT")
55+
56+
if mcp_port:
57+
run_http_with_uvicorn()
58+
else:
59+
run_stdio()
60+
61+
62+
if __name__ == "__main__":
63+
run_mcp_server()
Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
"""GitGuardian MCP server for developers with remediation tools."""
22

33
import logging
4-
import os
54

65
from gg_api_core.mcp_server import get_mcp_server
76
from gg_api_core.scopes import set_developer_scopes
8-
from gg_api_core.sentry_integration import init_sentry
97

108
from developer_mcp_server.add_health_check import add_health_check
119
from developer_mcp_server.register_tools import DEVELOPER_INSTRUCTIONS, register_developer_tools
@@ -21,40 +19,10 @@
2119
log_level="DEBUG",
2220
instructions=DEVELOPER_INSTRUCTIONS,
2321
)
24-
logger.info("Created Developer GitGuardianFastMCP instance")
2522

2623
register_developer_tools(mcp)
2724
add_health_check(mcp)
2825

29-
3026
set_developer_scopes()
3127

32-
33-
def run_mcp_server():
34-
logger.info("Starting Developer MCP server...")
35-
36-
# Initialize Sentry if configured (optional)
37-
sentry_enabled = init_sentry()
38-
if sentry_enabled:
39-
logger.info("Sentry monitoring is enabled")
40-
else:
41-
logger.debug("Sentry monitoring is not configured")
42-
43-
# Check if HTTP/SSE transport is requested via environment variables
44-
mcp_port = os.environ.get("MCP_PORT")
45-
mcp_host = os.environ.get("MCP_HOST", "127.0.0.1")
46-
47-
if mcp_port:
48-
# Use HTTP/SSE transport
49-
import uvicorn
50-
51-
logger.info(f"Starting MCP server with HTTP/SSE transport on {mcp_host}:{mcp_port}")
52-
uvicorn.run(mcp.sse_app(), host=mcp_host, port=int(mcp_port))
53-
else:
54-
# Use default stdio transport
55-
logger.info("Starting MCP server with stdio transport (default)")
56-
mcp.run()
57-
58-
59-
if __name__ == "__main__":
60-
run_mcp_server()
28+
logger.info("Developer MCP server instance created and configured")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from developer_mcp_server.server import mcp
2+
3+
sse_app = mcp.sse_app()

packages/secops_mcp_server/pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ dependencies = [
2929
"httpx>=0.24.0",
3030
"pydantic>=2.0.0",
3131
"gg-api-core",
32-
"uvicorn>=0.27.0"
32+
"uvicorn>=0.27.0",
33+
"gunicorn>=23.0.0"
3334
]
3435

3536
[project.optional-dependencies]
@@ -38,8 +39,11 @@ sentry = [
3839
]
3940

4041
[project.scripts]
41-
secops-mcp-server = "secops_mcp_server.server:run_mcp_server"
42+
secops-mcp-server = "secops_mcp_server.run:run_mcp_server"
4243

4344
[build-system]
4445
requires = ["hatchling"]
4546
build-backend = "hatchling.build"
47+
48+
[tool.hatch.build.targets.wheel]
49+
packages = ["src/secops_mcp_server", "src/config"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Configuration modules for the MCP server."""
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Gunicorn configuration and custom logger for MCP server.
2+
3+
This module provides a custom logger class for gunicorn that integrates
4+
with Python's logging system for consistent log formatting.
5+
"""
6+
7+
import logging
8+
9+
10+
class GunicornLogger:
11+
"""Custom logger class for gunicorn.
12+
13+
This logger integrates gunicorn's logging with Python's standard logging
14+
to ensure consistent log formatting across the application.
15+
"""
16+
17+
def __init__(self, cfg):
18+
"""Initialize the GunicornLogger.
19+
20+
Args:
21+
cfg: Gunicorn configuration object
22+
"""
23+
self.cfg = cfg
24+
self._error_log = None
25+
self._access_log = None
26+
27+
@property
28+
def error_log(self):
29+
"""Get the error logger."""
30+
if not self._error_log:
31+
self._error_log = logging.getLogger("gunicorn.error")
32+
return self._error_log
33+
34+
@property
35+
def access_log(self):
36+
"""Get the access logger."""
37+
if not self._access_log:
38+
self._access_log = logging.getLogger("gunicorn.access")
39+
return self._access_log
40+
41+
def critical(self, msg, *args, **kwargs):
42+
"""Log critical message."""
43+
self.error_log.critical(msg, *args, **kwargs)
44+
45+
def error(self, msg, *args, **kwargs):
46+
"""Log error message."""
47+
self.error_log.error(msg, *args, **kwargs)
48+
49+
def warning(self, msg, *args, **kwargs):
50+
"""Log warning message."""
51+
self.error_log.warning(msg, *args, **kwargs)
52+
53+
def info(self, msg, *args, **kwargs):
54+
"""Log info message."""
55+
self.error_log.info(msg, *args, **kwargs)
56+
57+
def debug(self, msg, *args, **kwargs):
58+
"""Log debug message."""
59+
self.error_log.debug(msg, *args, **kwargs)
60+
61+
def exception(self, msg, *args, **kwargs):
62+
"""Log exception message."""
63+
self.error_log.exception(msg, *args, **kwargs)
64+
65+
def log(self, lvl, msg, *args, **kwargs):
66+
"""Log message at specific level."""
67+
self.error_log.log(lvl, msg, *args, **kwargs)
68+
69+
def access(self, resp, req, environ, request_time):
70+
"""Log access information."""
71+
self.access_log.info(
72+
'%s - "%s %s %s" %d',
73+
environ.get("REMOTE_ADDR", "-"),
74+
environ.get("REQUEST_METHOD", "-"),
75+
environ.get("PATH_INFO", "-"),
76+
environ.get("SERVER_PROTOCOL", "-"),
77+
resp.status_code,
78+
)
79+
80+
def reopen_files(self):
81+
"""Reopen log files (for log rotation)."""
82+
pass
83+
84+
def close_on_exec(self):
85+
"""Close log files on exec."""
86+
pass
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Runtime entry points for the SecOps MCP server.
2+
3+
This module provides different ways to run the MCP server:
4+
- stdio: Standard input/output transport (default for CLI tools)
5+
- http: HTTP/SSE transport using uvicorn (for local development)
6+
"""
7+
8+
import logging
9+
import os
10+
11+
from gg_api_core.sentry_integration import init_sentry
12+
13+
from secops_mcp_server.server import mcp
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
def run_stdio():
19+
"""Run the MCP server over stdio transport.
20+
21+
This is the default mode for MCP servers, used when the server
22+
is invoked as a subprocess by MCP clients like Claude Desktop.
23+
"""
24+
init_sentry()
25+
# Run the server in stdio mode
26+
logger.info("SecOps MCP server running on stdio")
27+
mcp.run()
28+
29+
30+
def run_http_with_uvicorn():
31+
"""Run the MCP server over HTTP using uvicorn ASGI server.
32+
33+
This is meant for local development. For production ready setup,
34+
better use gunicorn with uvicorn ASGI workers via ggmcp_http.sse_app:app
35+
"""
36+
37+
init_sentry()
38+
# Get host and port from environment variables
39+
mcp_port = int(os.environ.get("MCP_PORT", "8000"))
40+
mcp_host = os.environ.get("MCP_HOST", "127.0.0.1")
41+
42+
# Use HTTP/SSE transport with uvicorn
43+
import uvicorn
44+
45+
logger.info(f"Starting SecOps MCP server on {mcp_host}:{mcp_port}")
46+
uvicorn.run(mcp.sse_app(), host=mcp_host, port=mcp_port)
47+
48+
49+
def run_mcp_server():
50+
"""Run the MCP server with transport auto-detection.
51+
52+
If MCP_PORT is set, uses HTTP/SSE transport.
53+
Otherwise, uses stdio transport (default).
54+
"""
55+
mcp_port = os.environ.get("MCP_PORT")
56+
57+
if mcp_port:
58+
run_http_with_uvicorn()
59+
else:
60+
run_stdio()
61+
62+
63+
if __name__ == "__main__":
64+
run_mcp_server()

packages/secops_mcp_server/src/secops_mcp_server/server.py

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
"""GitGuardian MCP server for SecOps teams with incident management tools."""
22

33
import logging
4-
import os
54
from typing import Any, Literal
65

76
from developer_mcp_server.add_health_check import add_health_check
87
from developer_mcp_server.register_tools import register_developer_tools
98
from fastmcp.exceptions import ToolError
109
from gg_api_core.mcp_server import get_mcp_server, register_common_tools
1110
from gg_api_core.scopes import set_secops_scopes
12-
from gg_api_core.sentry_integration import init_sentry
1311
from gg_api_core.tools.assign_incident import assign_incident
1412
from gg_api_core.tools.create_code_fix_request import create_code_fix_request
1513
from gg_api_core.tools.list_users import list_users
@@ -118,7 +116,6 @@ class ListHoneytokensParams(BaseModel):
118116
log_level="DEBUG",
119117
instructions=SECOPS_INSTRUCTIONS,
120118
)
121-
logger.debug("Created SecOps GitGuardianFastMCP instance")
122119

123120
register_developer_tools(mcp)
124121
add_health_check(mcp)
@@ -208,37 +205,4 @@ async def get_current_token_info() -> dict[str, Any]:
208205

209206
register_common_tools(mcp)
210207

211-
212-
def run_mcp_server():
213-
logger.info("Starting SecOps MCP server...")
214-
215-
# Initialize Sentry if configured (optional)
216-
sentry_enabled = init_sentry()
217-
if sentry_enabled:
218-
logger.info("Sentry monitoring is enabled")
219-
else:
220-
logger.debug("Sentry monitoring is not configured")
221-
222-
# Check if HTTP/SSE transport is requested via environment variables
223-
mcp_port = os.environ.get("MCP_PORT")
224-
mcp_host = os.environ.get("MCP_HOST", "127.0.0.1")
225-
226-
if mcp_port:
227-
# Use HTTP/SSE transport
228-
import uvicorn
229-
230-
logger.info(f"Starting MCP server with HTTP/SSE transport on {mcp_host}:{mcp_port}")
231-
uvicorn.run(mcp.sse_app(), host=mcp_host, port=int(mcp_port))
232-
else:
233-
# Use default stdio transport
234-
logger.info("Starting MCP server with stdio transport (default)")
235-
mcp.run()
236-
237-
238-
# Entrypoint for the Docker container and Kubernetes deployment
239-
def run_mcp_server_over_http():
240-
run_mcp_server()
241-
242-
243-
if __name__ == "__main__":
244-
run_mcp_server()
208+
logger.info("SecOps MCP server instance created and configured")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""ASGI application for MCP server over HTTP/SSE.
2+
3+
This module exports the ASGI application for use with ASGI servers like gunicorn + uvicorn.
4+
It imports the configured MCP server and exposes its SSE application.
5+
6+
This module is specifically for production deployment with gunicorn.
7+
For local development, use the run_http_with_uvicorn() function instead.
8+
"""
9+
10+
import logging
11+
12+
from gg_api_core.sentry_integration import init_sentry
13+
14+
from secops_mcp_server.server import mcp
15+
16+
logger = logging.getLogger(__name__)
17+
18+
# Initialize Sentry for production deployment
19+
init_sentry()
20+
21+
# Export the ASGI application
22+
# This will be used by gunicorn as: secops_mcp_server.sse_app:app
23+
app = mcp.sse_app()
24+
25+
logger.info("MCP SSE application initialized for HTTP/SSE transport")

0 commit comments

Comments
 (0)