langprotect-mcp-gateway 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

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.
@@ -1,26 +1,6 @@
1
1
  #!/usr/bin/env python3
2
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)
3
+ LangProtect MCP Gateway - Security Gateway for MCP Servers
24
4
  """
25
5
 
26
6
  import sys
@@ -28,86 +8,82 @@ import json
28
8
  import os
29
9
  import subprocess
30
10
  import requests
31
- import threading
32
- import time
11
+ import argparse
33
12
  from datetime import datetime, timedelta
34
- from pathlib import Path
35
13
  from typing import Dict, List, Any, Optional
36
14
  import logging
37
15
 
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
- )
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)])
44
18
  logger = logging.getLogger('langprotect-gateway')
45
19
 
46
20
 
47
21
  class MCPServer:
48
- """Represents a single MCP server that can be called"""
49
-
50
22
  def __init__(self, name: str, config: Dict[str, Any]):
51
23
  self.name = name
52
24
  self.command = config.get('command')
53
25
  self.args = config.get('args', [])
54
26
  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}")
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)})")
58
31
 
59
- def call(self, method: str, params: Dict) -> Dict:
60
- """Call this MCP server with a method and params"""
32
+ def _next_id(self) -> int:
33
+ self._request_id += 1
34
+ return self._request_id
35
+
36
+ def start(self) -> bool:
61
37
  try:
62
- # Start process for this call
63
38
  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
-
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:
84
74
  self.process.stdin.write(json.dumps(request) + "\n")
85
75
  self.process.stdin.flush()
86
-
87
- # Read response
88
76
  response_line = self.process.stdout.readline()
89
77
  if not response_line:
90
- error_output = self.process.stderr.read()
91
- raise Exception(f"No response from {self.name}: {error_output}")
92
-
78
+ raise Exception(f"No response from {self.name}")
93
79
  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
-
80
+ logger.debug(f"<- {self.name}: {str(response)[:200]}")
101
81
  return response
102
-
103
82
  except Exception as e:
104
- logger.error(f"Error calling {self.name}: {e}")
105
- if self.process:
106
- self.process.terminate()
83
+ logger.error(f"Error calling {self.name}.{method}: {e}")
107
84
  raise
108
85
 
109
86
  def discover_tools(self) -> List[Dict]:
110
- """Discover what tools this MCP server provides"""
111
87
  try:
112
88
  response = self.call("tools/list", {})
113
89
  if "result" in response:
@@ -120,407 +96,286 @@ class MCPServer:
120
96
  return []
121
97
 
122
98
 
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")
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
156
106
 
157
- def login(self):
158
- """Authenticate with LangProtect backend and get JWT token"""
107
+ def login(self) -> bool:
159
108
  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
-
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)
171
111
  if response.status_code == 200:
172
112
  data = response.json()
173
113
  self.jwt_token = data.get('access_token')
174
114
  self.token_expiry = datetime.now() + timedelta(days=6)
175
- logger.info("Authentication successful")
115
+ logger.info("Authentication successful")
176
116
  return True
177
117
  else:
178
- logger.error(f"Login failed: {response.status_code} - {response.text}")
179
- sys.exit(1)
180
-
118
+ logger.error(f"Login failed: {response.status_code}")
119
+ return False
181
120
  except Exception as e:
182
- logger.error(f"Login error: {e}")
183
- sys.exit(1)
121
+ logger.error(f"Login error: {e}")
122
+ return False
184
123
 
185
- def ensure_token(self):
186
- """Ensure we have a valid JWT token"""
124
+ def ensure_token(self) -> bool:
187
125
  if not self.jwt_token or (self.token_expiry and datetime.now() > self.token_expiry):
188
- logger.info("Token expired, re-authenticating...")
189
126
  return self.login()
190
127
  return True
191
128
 
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()
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.mcp_json_path = mcp_json_path
200
154
 
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
- ]
155
+ # Load credentials from env vars first, then potentially from config
156
+ self.langprotect_url = os.getenv('LANGPROTECT_URL', 'http://localhost:8000')
157
+ self.email = os.getenv('LANGPROTECT_EMAIL')
158
+ self.password = os.getenv('LANGPROTECT_PASSWORD')
213
159
 
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}")
160
+ # Try to load credentials from mcp.json env section (like Lasso)
161
+ if mcp_json_path and (not self.email or not self.password):
162
+ self._load_env_from_config(mcp_json_path)
253
163
 
254
- logger.warning("⚠️ No MCP config found - gateway will not proxy any servers")
164
+ self.auth: Optional[LangProtectAuth] = None
165
+ self.mcp_servers: Dict[str, MCPServer] = {}
166
+ self.tool_to_server: Dict[str, str] = {}
167
+ self.all_tools: List[Dict] = []
168
+ logger.debug(f"LANGPROTECT_URL: {self.langprotect_url}")
169
+ logger.debug(f"LANGPROTECT_EMAIL: {self.email}")
255
170
 
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
-
171
+ def _load_env_from_config(self, path: str):
172
+ """Load credentials from mcp.json env section (Lasso-style)"""
260
173
  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
- })
174
+ expanded_path = os.path.expanduser(path)
175
+ with open(expanded_path, 'r') as f:
176
+ config = json.load(f)
277
177
 
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
- }
178
+ # Look for env vars in the gateway's config section
179
+ mcp_servers = config.get('mcpServers', {})
180
+ for gateway_name in ['langprotect-gateway', 'langprotect', 'mcp-gateway']:
181
+ gateway_config = mcp_servers.get(gateway_name, {})
182
+ env_section = gateway_config.get('env', {})
183
+ if env_section:
184
+ if not self.langprotect_url or self.langprotect_url == 'http://localhost:8000':
185
+ self.langprotect_url = env_section.get('LANGPROTECT_URL', self.langprotect_url)
186
+ if not self.email:
187
+ self.email = env_section.get('LANGPROTECT_EMAIL')
188
+ if not self.password:
189
+ self.password = env_section.get('LANGPROTECT_PASSWORD')
190
+ logger.info(f"Loaded credentials from config env section")
191
+ break
192
+ except Exception as e:
193
+ logger.debug(f"Could not load env from config: {e}")
194
+
195
+ def initialize(self) -> bool:
196
+ if self.email and self.password:
197
+ self.auth = LangProtectAuth(self.langprotect_url, self.email, self.password)
198
+ if not self.auth.login():
199
+ logger.error("Failed to authenticate with LangProtect backend")
200
+ return False
201
+ else:
202
+ logger.warning("No LangProtect credentials - running in pass-through mode")
203
+ if not self.load_servers():
204
+ return False
205
+ if not self.start_servers():
206
+ return False
207
+ logger.info("=" * 50)
208
+ logger.info("LangProtect Gateway initialized")
209
+ logger.info(f"Backend: {self.langprotect_url}")
210
+ logger.info(f"Servers: {len(self.mcp_servers)}")
211
+ logger.info(f"Tools: {len(self.all_tools)}")
212
+ logger.info("=" * 50)
213
+ return True
214
+
215
+ def load_servers(self) -> bool:
216
+ # Mode 1: Single server via environment variables (for wrapper scripts)
217
+ mcp_command = os.getenv('MCP_SERVER_COMMAND')
218
+ mcp_args = os.getenv('MCP_SERVER_ARGS')
219
+ if mcp_command:
220
+ logger.info(f"Single server mode: {mcp_command}")
221
+ args_list = [arg.strip() for arg in mcp_args.split(',')] if mcp_args else []
222
+ server_name = os.getenv('MCP_SERVER_NAME', 'proxied-server')
223
+ self.mcp_servers[server_name] = MCPServer(server_name, {'command': mcp_command, 'args': args_list, 'env': {}})
224
+ return True
225
+
226
+ # Mode 2: Config file (mcp.json)
227
+ if self.mcp_json_path:
228
+ return self.load_from_mcp_json(self.mcp_json_path)
229
+
230
+ logger.warning("No MCP servers configured")
231
+ return False
232
+
233
+ def load_from_mcp_json(self, path: str) -> bool:
234
+ try:
235
+ expanded_path = os.path.expanduser(path)
236
+ with open(expanded_path, 'r') as f:
237
+ config = json.load(f)
285
238
 
286
- logger.debug(f"Logging to backend: {method} on {server_name}")
239
+ # Try multiple config structures:
240
+ # 1. Lasso-style: mcpServers.langprotect-gateway.servers (nested)
241
+ # 2. VS Code style: servers (direct)
242
+ # 3. Claude Desktop style: mcpServers (direct)
287
243
 
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
- )
244
+ servers = {}
297
245
 
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}'}
246
+ # Check for Lasso-style nested config
247
+ mcp_servers = config.get('mcpServers', {})
248
+ for gateway_name in ['langprotect-gateway', 'langprotect', 'mcp-gateway']:
249
+ gateway_config = mcp_servers.get(gateway_name, {})
250
+ if 'servers' in gateway_config:
251
+ servers = gateway_config['servers']
252
+ logger.info(f"Found nested servers config under mcpServers.{gateway_name}.servers")
253
+ break
302
254
 
303
- result = response.json()
304
- status = result.get('status', '').lower()
255
+ # Fallback to direct config
256
+ if not servers:
257
+ servers = config.get('servers', config.get('mcpServers', {}))
305
258
 
306
- logger.debug(f"Backend response: status={status}, log_id={result.get('id')}")
259
+ if not servers:
260
+ logger.error("No servers found in config file")
261
+ return False
307
262
 
308
- return result
263
+ for name, cfg in servers.items():
264
+ # Skip gateway self-references
265
+ if name in ['langprotect-gateway', 'langprotect', 'mcp-gateway']:
266
+ continue
267
+ self.mcp_servers[name] = MCPServer(name, cfg)
309
268
 
269
+ logger.info(f"Loaded {len(self.mcp_servers)} servers from config")
270
+ return len(self.mcp_servers) > 0
310
271
  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)}
272
+ logger.error(f"Error loading {path}: {e}")
273
+ return False
274
+
275
+ def start_servers(self) -> bool:
276
+ started = 0
277
+ for name, server in list(self.mcp_servers.items()):
278
+ if server.start():
279
+ tools = server.discover_tools()
280
+ for tool in tools:
281
+ tool_name = tool.get('name')
282
+ if tool_name:
283
+ self.tool_to_server[tool_name] = name
284
+ tool_copy = tool.copy()
285
+ tool_copy['description'] = f"[{name}] {tool_copy.get('description', '')}"
286
+ self.all_tools.append(tool_copy)
287
+ started += 1
288
+ else:
289
+ del self.mcp_servers[name]
290
+ return started > 0
314
291
 
315
- def handle_mcp_request(self, request: Dict) -> Dict:
316
- """Main MCP protocol handler"""
292
+ def shutdown(self):
293
+ for server in self.mcp_servers.values():
294
+ server.stop()
295
+
296
+ def handle_request(self, request: Dict) -> Optional[Dict]:
317
297
  method = request.get('method')
318
298
  request_id = request.get('id')
319
-
320
- logger.info(f"📥 MCP Request: {method} (id={request_id})")
321
-
299
+ params = request.get('params', {})
300
+ logger.info(f"Request: {method} (id={request_id})")
322
301
  try:
323
302
  if method == 'initialize':
324
- return self.handle_initialize(request)
303
+ return {'jsonrpc': '2.0', 'id': request_id, 'result': {'protocolVersion': '2024-11-05', 'capabilities': {'tools': {}}, 'serverInfo': {'name': 'langprotect-gateway', 'version': '1.0.0'}}}
304
+ elif method == 'notifications/initialized':
305
+ return None
325
306
  elif method == 'tools/list':
326
- return self.handle_list_tools(request)
307
+ logger.info(f"Returning {len(self.all_tools)} tools")
308
+ return {'jsonrpc': '2.0', 'id': request_id, 'result': {'tools': self.all_tools}}
327
309
  elif method == 'tools/call':
328
- return self.handle_call_tool(request)
310
+ return self._handle_call_tool(request_id, params)
329
311
  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
-
312
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32601, 'message': f'Method not found: {method}'}}
340
313
  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
- }
314
+ logger.error(f"Error handling {method}: {e}")
315
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32603, 'message': str(e)}}
394
316
 
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')
317
+ def _handle_call_tool(self, request_id, params: Dict) -> Dict:
318
+ tool_name = params.get('name', '')
399
319
  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
320
  server_name = self.tool_to_server.get(tool_name)
406
321
  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
-
322
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32602, 'message': f'Unknown tool: {tool_name}'}}
323
+ server = self.mcp_servers.get(server_name)
324
+ if not server:
325
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32602, 'message': f'Server not found: {server_name}'}}
326
+ logger.info(f"Tool call: {server_name}.{tool_name}")
327
+ if self.auth:
328
+ scan_result = self.auth.scan(tool_name, arguments, server_name)
329
+ status = scan_result.get('status', '').lower()
330
+ if status == 'blocked':
331
+ reason = scan_result.get('detections', {}).get('MCPActionControl', {}).get('reason', 'Policy violation')
332
+ logger.warning(f"BLOCKED: {tool_name} - {reason}")
333
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32000, 'message': f'LangProtect: {reason}'}}
334
+ logger.info(f"ALLOWED (log_id={scan_result.get('id')})")
447
335
  try:
448
- server = self.mcp_servers[server_name]
449
- response = server.call('tools/call', params)
450
-
451
- # Log successful response
336
+ response = server.call('tools/call', {'name': tool_name, 'arguments': arguments})
452
337
  if 'result' in response:
453
- logger.info(f"✅ Tool {tool_name} completed successfully")
338
+ return {'jsonrpc': '2.0', 'id': request_id, 'result': response['result']}
454
339
  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')
340
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': response['error']}
459
341
  return response
460
-
461
342
  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
- }
343
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32603, 'message': f'Error executing tool: {e}'}}
471
344
 
472
345
  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
346
  try:
481
347
  for line in sys.stdin:
482
348
  line = line.strip()
483
349
  if not line:
484
350
  continue
485
-
486
351
  try:
487
352
  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
-
353
+ response = self.handle_request(request)
354
+ if response:
355
+ print(json.dumps(response), flush=True)
493
356
  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
-
357
+ print(json.dumps({'jsonrpc': '2.0', 'error': {'code': -32700, 'message': f'Parse error: {e}'}}), flush=True)
504
358
  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)
359
+ pass
360
+ finally:
361
+ self.shutdown()
362
+
363
+
364
+ def parse_args():
365
+ parser = argparse.ArgumentParser(description='LangProtect MCP Gateway')
366
+ parser.add_argument('--mcp-json-path', type=str, help='Path to mcp.json configuration file')
367
+ parser.add_argument('--debug', action='store_true', help='Enable debug logging')
368
+ return parser.parse_args()
509
369
 
510
370
 
511
371
  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")
372
+ args = parse_args()
373
+ if args.debug:
374
+ os.environ['DEBUG'] = 'true'
375
+ logging.getLogger('langprotect-gateway').setLevel(logging.DEBUG)
376
+ gateway = LangProtectGateway(mcp_json_path=args.mcp_json_path)
377
+ if not gateway.initialize():
520
378
  sys.exit(1)
521
-
522
- # Start gateway
523
- gateway = LangProtectGateway()
524
379
  gateway.run()
525
380
 
526
381