langprotect-mcp-gateway 1.0.0__py3-none-any.whl → 1.1.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.
@@ -14,7 +14,7 @@ Or via command line:
14
14
  langprotect-gateway
15
15
  """
16
16
 
17
- __version__ = '1.0.0'
17
+ __version__ = '1.1.0'
18
18
  __author__ = 'LangProtect Security Team'
19
19
  __license__ = 'MIT'
20
20
 
@@ -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,224 @@ class MCPServer:
120
96
  return []
121
97
 
122
98
 
123
- class LangProtectGateway:
124
- """Main gateway that intercepts and logs all MCP traffic"""
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
125
106
 
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"""
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()
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"""
129
+ def scan(self, tool_name: str, arguments: Dict, server_name: str) -> Dict:
258
130
  self.ensure_token()
259
-
260
131
  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
-
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)
298
134
  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
135
+ logger.warning(f"Backend returned {response.status_code}, allowing request (fail-open)")
301
136
  return {'status': 'allowed', 'error': f'Backend error: {response.status_code}'}
302
-
303
137
  result = response.json()
304
- status = result.get('status', '').lower()
305
-
306
- logger.debug(f"Backend response: status={status}, log_id={result.get('id')}")
307
-
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'}
308
142
  return result
309
-
143
+ except requests.exceptions.Timeout:
144
+ logger.warning("Backend scan timeout, allowing request (fail-open)")
145
+ return {'status': 'allowed', 'error': 'Request timeout'}
310
146
  except Exception as e:
311
- logger.error(f"Error logging to backend: {e}")
312
- # Fail-open for now (TODO: make configurable)
147
+ logger.error(f"Scan error: {e}")
313
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
314
183
 
315
- def handle_mcp_request(self, request: Dict) -> Dict:
316
- """Main MCP protocol handler"""
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]:
317
235
  method = request.get('method')
318
236
  request_id = request.get('id')
319
-
320
- logger.info(f"📥 MCP Request: {method} (id={request_id})")
321
-
237
+ params = request.get('params', {})
238
+ logger.info(f"Request: {method} (id={request_id})")
322
239
  try:
323
240
  if method == 'initialize':
324
- return self.handle_initialize(request)
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
325
244
  elif method == 'tools/list':
326
- return self.handle_list_tools(request)
245
+ logger.info(f"Returning {len(self.all_tools)} tools")
246
+ return {'jsonrpc': '2.0', 'id': request_id, 'result': {'tools': self.all_tools}}
327
247
  elif method == 'tools/call':
328
- return self.handle_call_tool(request)
248
+ return self._handle_call_tool(request_id, params)
329
249
  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
-
250
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32601, 'message': f'Method not found: {method}'}}
340
251
  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
- }
252
+ logger.error(f"Error handling {method}: {e}")
253
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32603, 'message': str(e)}}
369
254
 
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')
255
+ def _handle_call_tool(self, request_id, params: Dict) -> Dict:
256
+ tool_name = params.get('name', '')
399
257
  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
258
  server_name = self.tool_to_server.get(tool_name)
406
259
  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
-
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')})")
447
273
  try:
448
- server = self.mcp_servers[server_name]
449
- response = server.call('tools/call', params)
450
-
451
- # Log successful response
274
+ response = server.call('tools/call', {'name': tool_name, 'arguments': arguments})
452
275
  if 'result' in response:
453
- logger.info(f"✅ Tool {tool_name} completed successfully")
276
+ return {'jsonrpc': '2.0', 'id': request_id, 'result': response['result']}
454
277
  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')
278
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': response['error']}
459
279
  return response
460
-
461
280
  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
- }
281
+ return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': -32603, 'message': f'Error executing tool: {e}'}}
471
282
 
472
283
  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
284
  try:
481
285
  for line in sys.stdin:
482
286
  line = line.strip()
483
287
  if not line:
484
288
  continue
485
-
486
289
  try:
487
290
  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
-
291
+ response = self.handle_request(request)
292
+ if response:
293
+ print(json.dumps(response), flush=True)
493
294
  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
-
295
+ print(json.dumps({'jsonrpc': '2.0', 'error': {'code': -32700, 'message': f'Parse error: {e}'}}), flush=True)
504
296
  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)
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()
509
307
 
510
308
 
511
309
  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")
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():
520
316
  sys.exit(1)
521
-
522
- # Start gateway
523
- gateway = LangProtectGateway()
524
317
  gateway.run()
525
318
 
526
319
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langprotect-mcp-gateway
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Security gateway for Model Context Protocol (MCP) to protect AI tool interactions
5
5
  Author-email: LangProtect Security Team <security@langprotect.com>
6
6
  License: MIT
@@ -0,0 +1,8 @@
1
+ langprotect_mcp_gateway/__init__.py,sha256=tRGe-nBw57h5EtB27h7RrUPcZTzuLylVzl8-REZzzDU,510
2
+ langprotect_mcp_gateway/gateway.py,sha256=Xw88Zo7EXDPyTjX0FJ-txeFqQLlCHmG2lwdSBiYRoIg,14727
3
+ langprotect_mcp_gateway-1.1.0.dist-info/licenses/LICENSE,sha256=aoVP65gKtirVmFPToow5L9IKN4FNjfM6Sejq_5b4cbM,1082
4
+ langprotect_mcp_gateway-1.1.0.dist-info/METADATA,sha256=XtdiT6OJnC_U-6bZ53NoNug3F21FJgRW_IZd7oQopJk,6152
5
+ langprotect_mcp_gateway-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ langprotect_mcp_gateway-1.1.0.dist-info/entry_points.txt,sha256=iM5-7ReYo6_nFF-2DHK1cSi1Nj6wGsG4QqJgcNZ7_GE,69
7
+ langprotect_mcp_gateway-1.1.0.dist-info/top_level.txt,sha256=UjNlX13ma4nwJXuEyi9eMX251c5rooeEao4zajX6ZHk,24
8
+ langprotect_mcp_gateway-1.1.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- langprotect_mcp_gateway/__init__.py,sha256=RAVgb8Z_PR2GkmgpAok9G1LCtcSsOarmb01j1pO6asc,510
2
- langprotect_mcp_gateway/gateway.py,sha256=72D-rGfOU-BRzSGdEnCjVGNP1OoSr9wBCFx7Kjzq1_c,19901
3
- langprotect_mcp_gateway-1.0.0.dist-info/licenses/LICENSE,sha256=aoVP65gKtirVmFPToow5L9IKN4FNjfM6Sejq_5b4cbM,1082
4
- langprotect_mcp_gateway-1.0.0.dist-info/METADATA,sha256=s97MVu9Zj70QhxXH-sszyxyvSeYkXdc_ICa9cN4xyw4,6152
5
- langprotect_mcp_gateway-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
- langprotect_mcp_gateway-1.0.0.dist-info/entry_points.txt,sha256=iM5-7ReYo6_nFF-2DHK1cSi1Nj6wGsG4QqJgcNZ7_GE,69
7
- langprotect_mcp_gateway-1.0.0.dist-info/top_level.txt,sha256=UjNlX13ma4nwJXuEyi9eMX251c5rooeEao4zajX6ZHk,24
8
- langprotect_mcp_gateway-1.0.0.dist-info/RECORD,,