langprotect-mcp-gateway 1.0.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/PKG-INFO +1 -1
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/langprotect_mcp_gateway/__init__.py +1 -1
- langprotect_mcp_gateway-1.1.0/langprotect_mcp_gateway/gateway.py +321 -0
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/langprotect_mcp_gateway.egg-info/PKG-INFO +1 -1
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/pyproject.toml +1 -1
- langprotect_mcp_gateway-1.0.0/langprotect_mcp_gateway/gateway.py +0 -528
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/LICENSE +0 -0
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/README.md +0 -0
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/langprotect_mcp_gateway.egg-info/SOURCES.txt +0 -0
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/langprotect_mcp_gateway.egg-info/dependency_links.txt +0 -0
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/langprotect_mcp_gateway.egg-info/entry_points.txt +0 -0
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/langprotect_mcp_gateway.egg-info/requires.txt +0 -0
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/langprotect_mcp_gateway.egg-info/top_level.txt +0 -0
- {langprotect_mcp_gateway-1.0.0 → langprotect_mcp_gateway-1.1.0}/setup.cfg +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
LangProtect MCP Gateway - Security Gateway for MCP Servers
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import requests
|
|
11
|
+
import argparse
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from typing import Dict, List, Any, Optional
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
log_level = os.environ.get("LOGLEVEL", "DEBUG" if os.getenv('DEBUG', 'false').lower() == 'true' else "INFO").upper()
|
|
17
|
+
logging.basicConfig(level=getattr(logging, log_level), format='[%(asctime)s] %(levelname)s: %(message)s', handlers=[logging.StreamHandler(sys.stderr)])
|
|
18
|
+
logger = logging.getLogger('langprotect-gateway')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MCPServer:
|
|
22
|
+
def __init__(self, name: str, config: Dict[str, Any]):
|
|
23
|
+
self.name = name
|
|
24
|
+
self.command = config.get('command')
|
|
25
|
+
self.args = config.get('args', [])
|
|
26
|
+
self.env = config.get('env', {})
|
|
27
|
+
self.tools: List[Dict] = []
|
|
28
|
+
self.process: Optional[subprocess.Popen] = None
|
|
29
|
+
self._request_id = 0
|
|
30
|
+
logger.info(f"Configured MCP server: {name} ({self.command} {' '.join(self.args)})")
|
|
31
|
+
|
|
32
|
+
def _next_id(self) -> int:
|
|
33
|
+
self._request_id += 1
|
|
34
|
+
return self._request_id
|
|
35
|
+
|
|
36
|
+
def start(self) -> bool:
|
|
37
|
+
try:
|
|
38
|
+
env = {**os.environ, **self.env}
|
|
39
|
+
self.process = subprocess.Popen([self.command] + self.args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env, bufsize=1)
|
|
40
|
+
init_request = {"jsonrpc": "2.0", "id": self._next_id(), "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "langprotect-gateway", "version": "1.0.0"}}}
|
|
41
|
+
self.process.stdin.write(json.dumps(init_request) + "\n")
|
|
42
|
+
self.process.stdin.flush()
|
|
43
|
+
response_line = self.process.stdout.readline()
|
|
44
|
+
if not response_line:
|
|
45
|
+
logger.error(f"Failed to initialize {self.name}")
|
|
46
|
+
return False
|
|
47
|
+
response = json.loads(response_line)
|
|
48
|
+
if "error" in response:
|
|
49
|
+
logger.error(f"Initialize error for {self.name}: {response['error']}")
|
|
50
|
+
return False
|
|
51
|
+
self.process.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
|
|
52
|
+
self.process.stdin.flush()
|
|
53
|
+
logger.info(f"Started MCP server: {self.name}")
|
|
54
|
+
return True
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Failed to start {self.name}: {e}")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def stop(self):
|
|
60
|
+
if self.process:
|
|
61
|
+
try:
|
|
62
|
+
self.process.terminate()
|
|
63
|
+
self.process.wait(timeout=5)
|
|
64
|
+
except:
|
|
65
|
+
self.process.kill()
|
|
66
|
+
self.process = None
|
|
67
|
+
|
|
68
|
+
def call(self, method: str, params: Dict) -> Dict:
|
|
69
|
+
if not self.process or self.process.poll() is not None:
|
|
70
|
+
raise Exception(f"Server {self.name} is not running")
|
|
71
|
+
request = {"jsonrpc": "2.0", "id": self._next_id(), "method": method, "params": params}
|
|
72
|
+
logger.debug(f"-> {self.name}.{method}")
|
|
73
|
+
try:
|
|
74
|
+
self.process.stdin.write(json.dumps(request) + "\n")
|
|
75
|
+
self.process.stdin.flush()
|
|
76
|
+
response_line = self.process.stdout.readline()
|
|
77
|
+
if not response_line:
|
|
78
|
+
raise Exception(f"No response from {self.name}")
|
|
79
|
+
response = json.loads(response_line)
|
|
80
|
+
logger.debug(f"<- {self.name}: {str(response)[:200]}")
|
|
81
|
+
return response
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Error calling {self.name}.{method}: {e}")
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
def discover_tools(self) -> List[Dict]:
|
|
87
|
+
try:
|
|
88
|
+
response = self.call("tools/list", {})
|
|
89
|
+
if "result" in response:
|
|
90
|
+
self.tools = response["result"].get("tools", [])
|
|
91
|
+
logger.info(f"Discovered {len(self.tools)} tools from {self.name}")
|
|
92
|
+
return self.tools
|
|
93
|
+
return []
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning(f"Could not discover tools from {self.name}: {e}")
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class LangProtectAuth:
|
|
100
|
+
def __init__(self, url: str, email: str, password: str):
|
|
101
|
+
self.url = url
|
|
102
|
+
self.email = email
|
|
103
|
+
self.password = password
|
|
104
|
+
self.jwt_token: Optional[str] = None
|
|
105
|
+
self.token_expiry: Optional[datetime] = None
|
|
106
|
+
|
|
107
|
+
def login(self) -> bool:
|
|
108
|
+
try:
|
|
109
|
+
logger.info(f"Authenticating with {self.url}...")
|
|
110
|
+
response = requests.post(f"{self.url}/v1/group-users/signin", json={'email': self.email, 'password': self.password}, timeout=10)
|
|
111
|
+
if response.status_code == 200:
|
|
112
|
+
data = response.json()
|
|
113
|
+
self.jwt_token = data.get('access_token')
|
|
114
|
+
self.token_expiry = datetime.now() + timedelta(days=6)
|
|
115
|
+
logger.info("Authentication successful")
|
|
116
|
+
return True
|
|
117
|
+
else:
|
|
118
|
+
logger.error(f"Login failed: {response.status_code}")
|
|
119
|
+
return False
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Login error: {e}")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def ensure_token(self) -> bool:
|
|
125
|
+
if not self.jwt_token or (self.token_expiry and datetime.now() > self.token_expiry):
|
|
126
|
+
return self.login()
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def scan(self, tool_name: str, arguments: Dict, server_name: str) -> Dict:
|
|
130
|
+
self.ensure_token()
|
|
131
|
+
try:
|
|
132
|
+
payload = {'prompt': json.dumps({'tool': tool_name, 'arguments': arguments, 'server': server_name}), 'client_ip': '127.0.0.1', 'user_agent': f'LangProtect-MCP-Gateway/1.0 (server={server_name})', 'source': 'mcp-gateway'}
|
|
133
|
+
response = requests.post(f"{self.url}/v1/group-logs/scan", json=payload, headers={'Authorization': f'Bearer {self.jwt_token}', 'Content-Type': 'application/json'}, timeout=5)
|
|
134
|
+
if response.status_code != 200:
|
|
135
|
+
logger.warning(f"Backend returned {response.status_code}, allowing request (fail-open)")
|
|
136
|
+
return {'status': 'allowed', 'error': f'Backend error: {response.status_code}'}
|
|
137
|
+
result = response.json()
|
|
138
|
+
# Handle scan service timeout - fail open
|
|
139
|
+
if result.get('detections', {}).get('error') == 'Scan service timeout':
|
|
140
|
+
logger.warning("Scan service timeout, allowing request (fail-open)")
|
|
141
|
+
return {'status': 'allowed', 'id': result.get('id'), 'error': 'Scan timeout'}
|
|
142
|
+
return result
|
|
143
|
+
except requests.exceptions.Timeout:
|
|
144
|
+
logger.warning("Backend scan timeout, allowing request (fail-open)")
|
|
145
|
+
return {'status': 'allowed', 'error': 'Request timeout'}
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Scan error: {e}")
|
|
148
|
+
return {'status': 'allowed', 'error': str(e)}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class LangProtectGateway:
|
|
152
|
+
def __init__(self, mcp_json_path: Optional[str] = None):
|
|
153
|
+
self.langprotect_url = os.getenv('LANGPROTECT_URL', 'http://localhost:8000')
|
|
154
|
+
self.email = os.getenv('LANGPROTECT_EMAIL')
|
|
155
|
+
self.password = os.getenv('LANGPROTECT_PASSWORD')
|
|
156
|
+
self.mcp_json_path = mcp_json_path
|
|
157
|
+
self.auth: Optional[LangProtectAuth] = None
|
|
158
|
+
self.mcp_servers: Dict[str, MCPServer] = {}
|
|
159
|
+
self.tool_to_server: Dict[str, str] = {}
|
|
160
|
+
self.all_tools: List[Dict] = []
|
|
161
|
+
logger.debug(f"LANGPROTECT_URL: {self.langprotect_url}")
|
|
162
|
+
logger.debug(f"LANGPROTECT_EMAIL: {self.email}")
|
|
163
|
+
|
|
164
|
+
def initialize(self) -> bool:
|
|
165
|
+
if self.email and self.password:
|
|
166
|
+
self.auth = LangProtectAuth(self.langprotect_url, self.email, self.password)
|
|
167
|
+
if not self.auth.login():
|
|
168
|
+
logger.error("Failed to authenticate with LangProtect backend")
|
|
169
|
+
return False
|
|
170
|
+
else:
|
|
171
|
+
logger.warning("No LangProtect credentials - running in pass-through mode")
|
|
172
|
+
if not self.load_servers():
|
|
173
|
+
return False
|
|
174
|
+
if not self.start_servers():
|
|
175
|
+
return False
|
|
176
|
+
logger.info("=" * 50)
|
|
177
|
+
logger.info("LangProtect Gateway initialized")
|
|
178
|
+
logger.info(f"Backend: {self.langprotect_url}")
|
|
179
|
+
logger.info(f"Servers: {len(self.mcp_servers)}")
|
|
180
|
+
logger.info(f"Tools: {len(self.all_tools)}")
|
|
181
|
+
logger.info("=" * 50)
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
def load_servers(self) -> bool:
|
|
185
|
+
mcp_command = os.getenv('MCP_SERVER_COMMAND')
|
|
186
|
+
mcp_args = os.getenv('MCP_SERVER_ARGS')
|
|
187
|
+
if mcp_command:
|
|
188
|
+
logger.info(f"Single server mode: {mcp_command}")
|
|
189
|
+
args_list = [arg.strip() for arg in mcp_args.split(',')] if mcp_args else []
|
|
190
|
+
server_name = os.getenv('MCP_SERVER_NAME', 'proxied-server')
|
|
191
|
+
self.mcp_servers[server_name] = MCPServer(server_name, {'command': mcp_command, 'args': args_list, 'env': {}})
|
|
192
|
+
return True
|
|
193
|
+
if self.mcp_json_path:
|
|
194
|
+
return self.load_from_mcp_json(self.mcp_json_path)
|
|
195
|
+
logger.warning("No MCP servers configured")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def load_from_mcp_json(self, path: str) -> bool:
|
|
199
|
+
try:
|
|
200
|
+
with open(path, 'r') as f:
|
|
201
|
+
config = json.load(f)
|
|
202
|
+
servers = config.get('servers', config.get('mcpServers', {}))
|
|
203
|
+
if not servers:
|
|
204
|
+
return False
|
|
205
|
+
for name, cfg in servers.items():
|
|
206
|
+
if name not in ['langprotect-gateway', 'langprotect']:
|
|
207
|
+
self.mcp_servers[name] = MCPServer(name, cfg)
|
|
208
|
+
return len(self.mcp_servers) > 0
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Error loading {path}: {e}")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def start_servers(self) -> bool:
|
|
214
|
+
started = 0
|
|
215
|
+
for name, server in list(self.mcp_servers.items()):
|
|
216
|
+
if server.start():
|
|
217
|
+
tools = server.discover_tools()
|
|
218
|
+
for tool in tools:
|
|
219
|
+
tool_name = tool.get('name')
|
|
220
|
+
if tool_name:
|
|
221
|
+
self.tool_to_server[tool_name] = name
|
|
222
|
+
tool_copy = tool.copy()
|
|
223
|
+
tool_copy['description'] = f"[{name}] {tool_copy.get('description', '')}"
|
|
224
|
+
self.all_tools.append(tool_copy)
|
|
225
|
+
started += 1
|
|
226
|
+
else:
|
|
227
|
+
del self.mcp_servers[name]
|
|
228
|
+
return started > 0
|
|
229
|
+
|
|
230
|
+
def shutdown(self):
|
|
231
|
+
for server in self.mcp_servers.values():
|
|
232
|
+
server.stop()
|
|
233
|
+
|
|
234
|
+
def handle_request(self, request: Dict) -> Optional[Dict]:
|
|
235
|
+
method = request.get('method')
|
|
236
|
+
request_id = request.get('id')
|
|
237
|
+
params = request.get('params', {})
|
|
238
|
+
logger.info(f"Request: {method} (id={request_id})")
|
|
239
|
+
try:
|
|
240
|
+
if method == 'initialize':
|
|
241
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'result': {'protocolVersion': '2024-11-05', 'capabilities': {'tools': {}}, 'serverInfo': {'name': 'langprotect-gateway', 'version': '1.0.0'}}}
|
|
242
|
+
elif method == 'notifications/initialized':
|
|
243
|
+
return None
|
|
244
|
+
elif method == 'tools/list':
|
|
245
|
+
logger.info(f"Returning {len(self.all_tools)} tools")
|
|
246
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'result': {'tools': self.all_tools}}
|
|
247
|
+
elif method == 'tools/call':
|
|
248
|
+
return self._handle_call_tool(request_id, params)
|
|
249
|
+
else:
|
|
250
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32601, 'message': f'Method not found: {method}'}}
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.error(f"Error handling {method}: {e}")
|
|
253
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32603, 'message': str(e)}}
|
|
254
|
+
|
|
255
|
+
def _handle_call_tool(self, request_id, params: Dict) -> Dict:
|
|
256
|
+
tool_name = params.get('name', '')
|
|
257
|
+
arguments = params.get('arguments', {})
|
|
258
|
+
server_name = self.tool_to_server.get(tool_name)
|
|
259
|
+
if not server_name:
|
|
260
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32602, 'message': f'Unknown tool: {tool_name}'}}
|
|
261
|
+
server = self.mcp_servers.get(server_name)
|
|
262
|
+
if not server:
|
|
263
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32602, 'message': f'Server not found: {server_name}'}}
|
|
264
|
+
logger.info(f"Tool call: {server_name}.{tool_name}")
|
|
265
|
+
if self.auth:
|
|
266
|
+
scan_result = self.auth.scan(tool_name, arguments, server_name)
|
|
267
|
+
status = scan_result.get('status', '').lower()
|
|
268
|
+
if status == 'blocked':
|
|
269
|
+
reason = scan_result.get('detections', {}).get('MCPActionControl', {}).get('reason', 'Policy violation')
|
|
270
|
+
logger.warning(f"BLOCKED: {tool_name} - {reason}")
|
|
271
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32000, 'message': f'LangProtect: {reason}'}}
|
|
272
|
+
logger.info(f"ALLOWED (log_id={scan_result.get('id')})")
|
|
273
|
+
try:
|
|
274
|
+
response = server.call('tools/call', {'name': tool_name, 'arguments': arguments})
|
|
275
|
+
if 'result' in response:
|
|
276
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'result': response['result']}
|
|
277
|
+
elif 'error' in response:
|
|
278
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'error': response['error']}
|
|
279
|
+
return response
|
|
280
|
+
except Exception as e:
|
|
281
|
+
return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32603, 'message': f'Error executing tool: {e}'}}
|
|
282
|
+
|
|
283
|
+
def run(self):
|
|
284
|
+
try:
|
|
285
|
+
for line in sys.stdin:
|
|
286
|
+
line = line.strip()
|
|
287
|
+
if not line:
|
|
288
|
+
continue
|
|
289
|
+
try:
|
|
290
|
+
request = json.loads(line)
|
|
291
|
+
response = self.handle_request(request)
|
|
292
|
+
if response:
|
|
293
|
+
print(json.dumps(response), flush=True)
|
|
294
|
+
except json.JSONDecodeError as e:
|
|
295
|
+
print(json.dumps({'jsonrpc': '2.0', 'error': {'code': -32700, 'message': f'Parse error: {e}'}}), flush=True)
|
|
296
|
+
except KeyboardInterrupt:
|
|
297
|
+
pass
|
|
298
|
+
finally:
|
|
299
|
+
self.shutdown()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def parse_args():
|
|
303
|
+
parser = argparse.ArgumentParser(description='LangProtect MCP Gateway')
|
|
304
|
+
parser.add_argument('--mcp-json-path', type=str, help='Path to mcp.json configuration file')
|
|
305
|
+
parser.add_argument('--debug', action='store_true', help='Enable debug logging')
|
|
306
|
+
return parser.parse_args()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def main():
|
|
310
|
+
args = parse_args()
|
|
311
|
+
if args.debug:
|
|
312
|
+
os.environ['DEBUG'] = 'true'
|
|
313
|
+
logging.getLogger('langprotect-gateway').setLevel(logging.DEBUG)
|
|
314
|
+
gateway = LangProtectGateway(mcp_json_path=args.mcp_json_path)
|
|
315
|
+
if not gateway.initialize():
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
gateway.run()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == '__main__':
|
|
321
|
+
main()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langprotect-mcp-gateway"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Security gateway for Model Context Protocol (MCP) to protect AI tool interactions"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -1,528 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
LangProtect MCP Gateway - Universal MCP Logging & Security
|
|
4
|
-
|
|
5
|
-
This gateway intercepts ALL MCP tool calls from any client (Claude, VS Code, etc.)
|
|
6
|
-
and logs them to LangProtect backend for security scanning and auditing.
|
|
7
|
-
|
|
8
|
-
Architecture:
|
|
9
|
-
1. Reads user's MCP config to discover other MCP servers
|
|
10
|
-
2. Implements MCP protocol (initialize, list_tools, call_tool)
|
|
11
|
-
3. Logs every tool call to LangProtect backend
|
|
12
|
-
4. Forwards requests to actual MCP servers
|
|
13
|
-
5. Returns responses to AI client
|
|
14
|
-
|
|
15
|
-
Usage:
|
|
16
|
-
python3 langprotect_gateway.py
|
|
17
|
-
|
|
18
|
-
Configuration (via environment variables):
|
|
19
|
-
LANGPROTECT_URL - Backend API URL (default: http://localhost:8000)
|
|
20
|
-
LANGPROTECT_EMAIL - User email for authentication
|
|
21
|
-
LANGPROTECT_PASSWORD - User password
|
|
22
|
-
MCP_CONFIG_PATH - Path to mcp.json (auto-detected if not set)
|
|
23
|
-
DEBUG - Enable debug logging (true/false)
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
import sys
|
|
27
|
-
import json
|
|
28
|
-
import os
|
|
29
|
-
import subprocess
|
|
30
|
-
import requests
|
|
31
|
-
import threading
|
|
32
|
-
import time
|
|
33
|
-
from datetime import datetime, timedelta
|
|
34
|
-
from pathlib import Path
|
|
35
|
-
from typing import Dict, List, Any, Optional
|
|
36
|
-
import logging
|
|
37
|
-
|
|
38
|
-
# Configure logging
|
|
39
|
-
logging.basicConfig(
|
|
40
|
-
level=logging.DEBUG if os.getenv('DEBUG', 'false').lower() == 'true' else logging.INFO,
|
|
41
|
-
format='[%(asctime)s] %(levelname)s: %(message)s',
|
|
42
|
-
handlers=[logging.StreamHandler(sys.stderr)]
|
|
43
|
-
)
|
|
44
|
-
logger = logging.getLogger('langprotect-gateway')
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class MCPServer:
|
|
48
|
-
"""Represents a single MCP server that can be called"""
|
|
49
|
-
|
|
50
|
-
def __init__(self, name: str, config: Dict[str, Any]):
|
|
51
|
-
self.name = name
|
|
52
|
-
self.command = config.get('command')
|
|
53
|
-
self.args = config.get('args', [])
|
|
54
|
-
self.env = config.get('env', {})
|
|
55
|
-
self.tools = [] # Will be populated by discovery
|
|
56
|
-
self.process = None
|
|
57
|
-
logger.info(f"Initialized MCP server: {name}")
|
|
58
|
-
|
|
59
|
-
def call(self, method: str, params: Dict) -> Dict:
|
|
60
|
-
"""Call this MCP server with a method and params"""
|
|
61
|
-
try:
|
|
62
|
-
# Start process for this call
|
|
63
|
-
env = {**os.environ, **self.env}
|
|
64
|
-
|
|
65
|
-
self.process = subprocess.Popen(
|
|
66
|
-
[self.command] + self.args,
|
|
67
|
-
stdin=subprocess.PIPE,
|
|
68
|
-
stdout=subprocess.PIPE,
|
|
69
|
-
stderr=subprocess.PIPE,
|
|
70
|
-
text=True,
|
|
71
|
-
env=env
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Send request
|
|
75
|
-
request = {
|
|
76
|
-
"jsonrpc": "2.0",
|
|
77
|
-
"id": 1,
|
|
78
|
-
"method": method,
|
|
79
|
-
"params": params
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
logger.debug(f"Calling {self.name}.{method}: {json.dumps(request)}")
|
|
83
|
-
|
|
84
|
-
self.process.stdin.write(json.dumps(request) + "\n")
|
|
85
|
-
self.process.stdin.flush()
|
|
86
|
-
|
|
87
|
-
# Read response
|
|
88
|
-
response_line = self.process.stdout.readline()
|
|
89
|
-
if not response_line:
|
|
90
|
-
error_output = self.process.stderr.read()
|
|
91
|
-
raise Exception(f"No response from {self.name}: {error_output}")
|
|
92
|
-
|
|
93
|
-
response = json.loads(response_line)
|
|
94
|
-
|
|
95
|
-
# Cleanup
|
|
96
|
-
self.process.terminate()
|
|
97
|
-
self.process.wait(timeout=1)
|
|
98
|
-
|
|
99
|
-
logger.debug(f"Response from {self.name}: {json.dumps(response)[:200]}")
|
|
100
|
-
|
|
101
|
-
return response
|
|
102
|
-
|
|
103
|
-
except Exception as e:
|
|
104
|
-
logger.error(f"Error calling {self.name}: {e}")
|
|
105
|
-
if self.process:
|
|
106
|
-
self.process.terminate()
|
|
107
|
-
raise
|
|
108
|
-
|
|
109
|
-
def discover_tools(self) -> List[Dict]:
|
|
110
|
-
"""Discover what tools this MCP server provides"""
|
|
111
|
-
try:
|
|
112
|
-
response = self.call("tools/list", {})
|
|
113
|
-
if "result" in response:
|
|
114
|
-
self.tools = response["result"].get("tools", [])
|
|
115
|
-
logger.info(f"Discovered {len(self.tools)} tools from {self.name}")
|
|
116
|
-
return self.tools
|
|
117
|
-
return []
|
|
118
|
-
except Exception as e:
|
|
119
|
-
logger.warning(f"Could not discover tools from {self.name}: {e}")
|
|
120
|
-
return []
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class LangProtectGateway:
|
|
124
|
-
"""Main gateway that intercepts and logs all MCP traffic"""
|
|
125
|
-
|
|
126
|
-
def __init__(self):
|
|
127
|
-
self.langprotect_url = os.getenv('LANGPROTECT_URL', 'http://localhost:8000')
|
|
128
|
-
self.email = os.getenv('LANGPROTECT_EMAIL')
|
|
129
|
-
self.password = os.getenv('LANGPROTECT_PASSWORD')
|
|
130
|
-
self.jwt_token = None
|
|
131
|
-
self.token_expiry = None
|
|
132
|
-
self.mcp_servers = {} # name -> MCPServer
|
|
133
|
-
self.tool_to_server = {} # tool_name -> server_name
|
|
134
|
-
|
|
135
|
-
# Debug: Log what we received
|
|
136
|
-
logger.debug(f"Environment variables received:")
|
|
137
|
-
logger.debug(f" LANGPROTECT_URL: {self.langprotect_url}")
|
|
138
|
-
logger.debug(f" LANGPROTECT_EMAIL: {self.email}")
|
|
139
|
-
logger.debug(f" LANGPROTECT_PASSWORD: {'***' + self.password[-4:] if self.password else 'NOT SET'}")
|
|
140
|
-
|
|
141
|
-
# Validate credentials
|
|
142
|
-
if not self.email or not self.password:
|
|
143
|
-
logger.error("LANGPROTECT_EMAIL and LANGPROTECT_PASSWORD must be set")
|
|
144
|
-
sys.exit(1)
|
|
145
|
-
|
|
146
|
-
# Authenticate
|
|
147
|
-
self.login()
|
|
148
|
-
|
|
149
|
-
# Discover MCP servers
|
|
150
|
-
self.discover_mcp_servers()
|
|
151
|
-
|
|
152
|
-
# Start token refresh thread
|
|
153
|
-
self.start_token_refresh()
|
|
154
|
-
|
|
155
|
-
logger.info("LangProtect Gateway initialized successfully")
|
|
156
|
-
|
|
157
|
-
def login(self):
|
|
158
|
-
"""Authenticate with LangProtect backend and get JWT token"""
|
|
159
|
-
try:
|
|
160
|
-
logger.info(f"Logging in to {self.langprotect_url}...")
|
|
161
|
-
|
|
162
|
-
response = requests.post(
|
|
163
|
-
f"{self.langprotect_url}/v1/group-users/signin",
|
|
164
|
-
json={
|
|
165
|
-
'email': self.email,
|
|
166
|
-
'password': self.password
|
|
167
|
-
},
|
|
168
|
-
timeout=10
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
if response.status_code == 200:
|
|
172
|
-
data = response.json()
|
|
173
|
-
self.jwt_token = data.get('access_token')
|
|
174
|
-
self.token_expiry = datetime.now() + timedelta(days=6)
|
|
175
|
-
logger.info("✅ Authentication successful")
|
|
176
|
-
return True
|
|
177
|
-
else:
|
|
178
|
-
logger.error(f"❌ Login failed: {response.status_code} - {response.text}")
|
|
179
|
-
sys.exit(1)
|
|
180
|
-
|
|
181
|
-
except Exception as e:
|
|
182
|
-
logger.error(f"❌ Login error: {e}")
|
|
183
|
-
sys.exit(1)
|
|
184
|
-
|
|
185
|
-
def ensure_token(self):
|
|
186
|
-
"""Ensure we have a valid JWT token"""
|
|
187
|
-
if not self.jwt_token or (self.token_expiry and datetime.now() > self.token_expiry):
|
|
188
|
-
logger.info("Token expired, re-authenticating...")
|
|
189
|
-
return self.login()
|
|
190
|
-
return True
|
|
191
|
-
|
|
192
|
-
def start_token_refresh(self):
|
|
193
|
-
"""Start background thread to refresh JWT token"""
|
|
194
|
-
def refresh_loop():
|
|
195
|
-
while True:
|
|
196
|
-
time.sleep(86400) # Check daily
|
|
197
|
-
if self.token_expiry and datetime.now() > self.token_expiry:
|
|
198
|
-
logger.info("Auto-refreshing token...")
|
|
199
|
-
self.login()
|
|
200
|
-
|
|
201
|
-
refresh_thread = threading.Thread(target=refresh_loop, daemon=True)
|
|
202
|
-
refresh_thread.start()
|
|
203
|
-
logger.debug("Token refresh thread started")
|
|
204
|
-
|
|
205
|
-
def discover_mcp_servers(self):
|
|
206
|
-
"""Find and load MCP servers from user's config"""
|
|
207
|
-
config_paths = [
|
|
208
|
-
os.getenv('MCP_CONFIG_PATH'),
|
|
209
|
-
os.path.expanduser('~/.cursor/mcp.json'),
|
|
210
|
-
os.path.expanduser('~/.config/Claude/claude_desktop_config.json'),
|
|
211
|
-
os.path.expanduser('~/Library/Application Support/Claude/claude_desktop_config.json')
|
|
212
|
-
]
|
|
213
|
-
|
|
214
|
-
for path in config_paths:
|
|
215
|
-
if not path:
|
|
216
|
-
continue
|
|
217
|
-
|
|
218
|
-
path = os.path.expanduser(path)
|
|
219
|
-
if os.path.exists(path):
|
|
220
|
-
logger.info(f"Found MCP config: {path}")
|
|
221
|
-
try:
|
|
222
|
-
with open(path, 'r') as f:
|
|
223
|
-
config = json.load(f)
|
|
224
|
-
|
|
225
|
-
servers = config.get('mcpServers', {})
|
|
226
|
-
|
|
227
|
-
# Load all servers except ourselves
|
|
228
|
-
for name, cfg in servers.items():
|
|
229
|
-
if name == 'langprotect-gateway' or name == 'langprotect':
|
|
230
|
-
continue
|
|
231
|
-
|
|
232
|
-
try:
|
|
233
|
-
server = MCPServer(name, cfg)
|
|
234
|
-
self.mcp_servers[name] = server
|
|
235
|
-
|
|
236
|
-
# Discover tools from this server
|
|
237
|
-
tools = server.discover_tools()
|
|
238
|
-
for tool in tools:
|
|
239
|
-
tool_name = tool.get('name')
|
|
240
|
-
if tool_name:
|
|
241
|
-
self.tool_to_server[tool_name] = name
|
|
242
|
-
logger.debug(f" Tool: {tool_name} -> {name}")
|
|
243
|
-
|
|
244
|
-
except Exception as e:
|
|
245
|
-
logger.warning(f"Failed to load MCP server {name}: {e}")
|
|
246
|
-
|
|
247
|
-
logger.info(f"✅ Loaded {len(self.mcp_servers)} MCP servers")
|
|
248
|
-
logger.info(f"✅ Discovered {len(self.tool_to_server)} tools")
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
except Exception as e:
|
|
252
|
-
logger.error(f"Error reading config {path}: {e}")
|
|
253
|
-
|
|
254
|
-
logger.warning("⚠️ No MCP config found - gateway will not proxy any servers")
|
|
255
|
-
|
|
256
|
-
def log_to_backend(self, method: str, params: Dict, server_name: str = "unknown") -> Dict:
|
|
257
|
-
"""Log MCP request to LangProtect backend for scanning"""
|
|
258
|
-
self.ensure_token()
|
|
259
|
-
|
|
260
|
-
try:
|
|
261
|
-
# Build content for scanning - convert MCP request to scannable format
|
|
262
|
-
if method == 'tools/call':
|
|
263
|
-
tool_name = params.get('name', 'unknown_tool')
|
|
264
|
-
arguments = params.get('arguments', {})
|
|
265
|
-
scan_content = json.dumps({
|
|
266
|
-
'tool': tool_name,
|
|
267
|
-
'arguments': arguments,
|
|
268
|
-
'server_url': server_name,
|
|
269
|
-
'method': method
|
|
270
|
-
})
|
|
271
|
-
else:
|
|
272
|
-
scan_content = json.dumps({
|
|
273
|
-
'method': method,
|
|
274
|
-
'params': params,
|
|
275
|
-
'server_url': server_name
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
# Use standard group scan endpoint (works with existing scanners + policies)
|
|
279
|
-
payload = {
|
|
280
|
-
'prompt': scan_content, # MCP request as prompt
|
|
281
|
-
'client_ip': '127.0.0.1',
|
|
282
|
-
'user_agent': f'LangProtect-MCP-Gateway/1.0 (server={server_name})',
|
|
283
|
-
'source': 'mcp-gateway'
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
logger.debug(f"Logging to backend: {method} on {server_name}")
|
|
287
|
-
|
|
288
|
-
response = requests.post(
|
|
289
|
-
f"{self.langprotect_url}/v1/group-logs/scan",
|
|
290
|
-
json=payload,
|
|
291
|
-
headers={
|
|
292
|
-
'Authorization': f'Bearer {self.jwt_token}',
|
|
293
|
-
'Content-Type': 'application/json'
|
|
294
|
-
},
|
|
295
|
-
timeout=30
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
if response.status_code != 200:
|
|
299
|
-
logger.error(f"Backend scan failed: {response.status_code} - {response.text}")
|
|
300
|
-
# Fail-open: allow request if backend error
|
|
301
|
-
return {'status': 'allowed', 'error': f'Backend error: {response.status_code}'}
|
|
302
|
-
|
|
303
|
-
result = response.json()
|
|
304
|
-
status = result.get('status', '').lower()
|
|
305
|
-
|
|
306
|
-
logger.debug(f"Backend response: status={status}, log_id={result.get('id')}")
|
|
307
|
-
|
|
308
|
-
return result
|
|
309
|
-
|
|
310
|
-
except Exception as e:
|
|
311
|
-
logger.error(f"Error logging to backend: {e}")
|
|
312
|
-
# Fail-open for now (TODO: make configurable)
|
|
313
|
-
return {'status': 'allowed', 'error': str(e)}
|
|
314
|
-
|
|
315
|
-
def handle_mcp_request(self, request: Dict) -> Dict:
|
|
316
|
-
"""Main MCP protocol handler"""
|
|
317
|
-
method = request.get('method')
|
|
318
|
-
request_id = request.get('id')
|
|
319
|
-
|
|
320
|
-
logger.info(f"📥 MCP Request: {method} (id={request_id})")
|
|
321
|
-
|
|
322
|
-
try:
|
|
323
|
-
if method == 'initialize':
|
|
324
|
-
return self.handle_initialize(request)
|
|
325
|
-
elif method == 'tools/list':
|
|
326
|
-
return self.handle_list_tools(request)
|
|
327
|
-
elif method == 'tools/call':
|
|
328
|
-
return self.handle_call_tool(request)
|
|
329
|
-
else:
|
|
330
|
-
logger.warning(f"Unknown method: {method}")
|
|
331
|
-
return {
|
|
332
|
-
'jsonrpc': '2.0',
|
|
333
|
-
'id': request_id,
|
|
334
|
-
'error': {
|
|
335
|
-
'code': -32601,
|
|
336
|
-
'message': f'Method not found: {method}'
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
except Exception as e:
|
|
341
|
-
logger.error(f"Error handling {method}: {e}", exc_info=True)
|
|
342
|
-
return {
|
|
343
|
-
'jsonrpc': '2.0',
|
|
344
|
-
'id': request_id,
|
|
345
|
-
'error': {
|
|
346
|
-
'code': -32603,
|
|
347
|
-
'message': f'Internal error: {str(e)}'
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
def handle_initialize(self, request: Dict) -> Dict:
|
|
352
|
-
"""Handle MCP initialize handshake"""
|
|
353
|
-
logger.info("Handling initialize request")
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
'jsonrpc': '2.0',
|
|
357
|
-
'id': request.get('id'),
|
|
358
|
-
'result': {
|
|
359
|
-
'protocolVersion': '2024-11-05',
|
|
360
|
-
'capabilities': {
|
|
361
|
-
'tools': {}
|
|
362
|
-
},
|
|
363
|
-
'serverInfo': {
|
|
364
|
-
'name': 'langprotect-gateway',
|
|
365
|
-
'version': '1.0.0'
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
def handle_list_tools(self, request: Dict) -> Dict:
|
|
371
|
-
"""Return all tools from all MCP servers"""
|
|
372
|
-
logger.info(f"Listing tools from {len(self.mcp_servers)} servers")
|
|
373
|
-
|
|
374
|
-
all_tools = []
|
|
375
|
-
|
|
376
|
-
for server_name, server in self.mcp_servers.items():
|
|
377
|
-
# Use cached tools from discovery
|
|
378
|
-
for tool in server.tools:
|
|
379
|
-
# Add server name to tool description for clarity
|
|
380
|
-
tool_copy = tool.copy()
|
|
381
|
-
original_desc = tool_copy.get('description', '')
|
|
382
|
-
tool_copy['description'] = f"[{server_name}] {original_desc}"
|
|
383
|
-
all_tools.append(tool_copy)
|
|
384
|
-
|
|
385
|
-
logger.info(f"✅ Returning {len(all_tools)} tools")
|
|
386
|
-
|
|
387
|
-
return {
|
|
388
|
-
'jsonrpc': '2.0',
|
|
389
|
-
'id': request.get('id'),
|
|
390
|
-
'result': {
|
|
391
|
-
'tools': all_tools
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
def handle_call_tool(self, request: Dict) -> Dict:
|
|
396
|
-
"""Intercept tool call, log it, check policy, forward to server"""
|
|
397
|
-
params = request.get('params', {})
|
|
398
|
-
tool_name = params.get('name')
|
|
399
|
-
arguments = params.get('arguments', {})
|
|
400
|
-
|
|
401
|
-
logger.info(f"🔧 Tool call: {tool_name}")
|
|
402
|
-
logger.debug(f" Arguments: {json.dumps(arguments)[:200]}")
|
|
403
|
-
|
|
404
|
-
# Find which server owns this tool
|
|
405
|
-
server_name = self.tool_to_server.get(tool_name)
|
|
406
|
-
if not server_name:
|
|
407
|
-
logger.error(f"Unknown tool: {tool_name}")
|
|
408
|
-
return {
|
|
409
|
-
'jsonrpc': '2.0',
|
|
410
|
-
'id': request.get('id'),
|
|
411
|
-
'error': {
|
|
412
|
-
'code': -32602,
|
|
413
|
-
'message': f'Unknown tool: {tool_name}'
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
# 1. Log to LangProtect backend (security scan)
|
|
418
|
-
log_result = self.log_to_backend('tools/call', params, server_name)
|
|
419
|
-
|
|
420
|
-
# 2. Check if blocked by policy
|
|
421
|
-
status = log_result.get('status', '').lower()
|
|
422
|
-
if status == 'blocked':
|
|
423
|
-
reason = 'Policy violation'
|
|
424
|
-
detections = log_result.get('detections', {})
|
|
425
|
-
if 'MCPActionControl' in detections:
|
|
426
|
-
reason = detections['MCPActionControl'].get('reason', reason)
|
|
427
|
-
|
|
428
|
-
logger.warning(f"🛡️ BLOCKED: {tool_name} - {reason}")
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
'jsonrpc': '2.0',
|
|
432
|
-
'id': request.get('id'),
|
|
433
|
-
'error': {
|
|
434
|
-
'code': -32000,
|
|
435
|
-
'message': f'🛡️ LangProtect: {reason}',
|
|
436
|
-
'data': {
|
|
437
|
-
'log_id': log_result.get('id'),
|
|
438
|
-
'risk_score': log_result.get('risk_score'),
|
|
439
|
-
'detections': detections
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
# 3. Forward to actual MCP server
|
|
445
|
-
logger.info(f"✅ ALLOWED: Forwarding to {server_name}")
|
|
446
|
-
|
|
447
|
-
try:
|
|
448
|
-
server = self.mcp_servers[server_name]
|
|
449
|
-
response = server.call('tools/call', params)
|
|
450
|
-
|
|
451
|
-
# Log successful response
|
|
452
|
-
if 'result' in response:
|
|
453
|
-
logger.info(f"✅ Tool {tool_name} completed successfully")
|
|
454
|
-
elif 'error' in response:
|
|
455
|
-
logger.warning(f"⚠️ Tool {tool_name} returned error: {response['error'].get('message')}")
|
|
456
|
-
|
|
457
|
-
# Return response with original request ID
|
|
458
|
-
response['id'] = request.get('id')
|
|
459
|
-
return response
|
|
460
|
-
|
|
461
|
-
except Exception as e:
|
|
462
|
-
logger.error(f"Error executing {tool_name} on {server_name}: {e}")
|
|
463
|
-
return {
|
|
464
|
-
'jsonrpc': '2.0',
|
|
465
|
-
'id': request.get('id'),
|
|
466
|
-
'error': {
|
|
467
|
-
'code': -32603,
|
|
468
|
-
'message': f'Error executing tool: {str(e)}'
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
def run(self):
|
|
473
|
-
"""Main loop: read MCP requests from stdin, write responses to stdout"""
|
|
474
|
-
logger.info("🚀 LangProtect Gateway started")
|
|
475
|
-
logger.info(f"📡 Connected to: {self.langprotect_url}")
|
|
476
|
-
logger.info(f"👤 User: {self.email}")
|
|
477
|
-
logger.info(f"🔧 Proxying {len(self.mcp_servers)} MCP servers")
|
|
478
|
-
logger.info("=" * 60)
|
|
479
|
-
|
|
480
|
-
try:
|
|
481
|
-
for line in sys.stdin:
|
|
482
|
-
line = line.strip()
|
|
483
|
-
if not line:
|
|
484
|
-
continue
|
|
485
|
-
|
|
486
|
-
try:
|
|
487
|
-
request = json.loads(line)
|
|
488
|
-
response = self.handle_mcp_request(request)
|
|
489
|
-
|
|
490
|
-
# Write response to stdout (MCP protocol)
|
|
491
|
-
print(json.dumps(response), flush=True)
|
|
492
|
-
|
|
493
|
-
except json.JSONDecodeError as e:
|
|
494
|
-
logger.error(f"Invalid JSON: {e}")
|
|
495
|
-
error_response = {
|
|
496
|
-
'jsonrpc': '2.0',
|
|
497
|
-
'error': {
|
|
498
|
-
'code': -32700,
|
|
499
|
-
'message': f'Parse error: {str(e)}'
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
print(json.dumps(error_response), flush=True)
|
|
503
|
-
|
|
504
|
-
except KeyboardInterrupt:
|
|
505
|
-
logger.info("\n👋 Gateway shutting down...")
|
|
506
|
-
except Exception as e:
|
|
507
|
-
logger.error(f"Fatal error: {e}", exc_info=True)
|
|
508
|
-
sys.exit(1)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
def main():
|
|
512
|
-
"""Entry point"""
|
|
513
|
-
# Check for setup mode
|
|
514
|
-
if len(sys.argv) > 1 and sys.argv[1] == 'setup':
|
|
515
|
-
logger.info("⚠️ Setup mode not yet implemented")
|
|
516
|
-
logger.info("Please set environment variables:")
|
|
517
|
-
logger.info(" LANGPROTECT_URL=https://api.langprotect.com")
|
|
518
|
-
logger.info(" LANGPROTECT_EMAIL=your@email.com")
|
|
519
|
-
logger.info(" LANGPROTECT_PASSWORD=yourpassword")
|
|
520
|
-
sys.exit(1)
|
|
521
|
-
|
|
522
|
-
# Start gateway
|
|
523
|
-
gateway = LangProtectGateway()
|
|
524
|
-
gateway.run()
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if __name__ == '__main__':
|
|
528
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|