Skip to content

Commit 1308c1c

Browse files
committed
- Make sure the version will not be include when search for the package in npm and git.
- Increase version - Add more details for the pypi - Add docs about the new scanner
1 parent d936301 commit 1308c1c

File tree

7 files changed

+235
-30
lines changed

7 files changed

+235
-30
lines changed

README.md

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
1-
# MCP Gateway
21

3-
![Hugging Face Token Masking Example](docs/MCP_Flow.png)
2+
<div align="center">
3+
<a href="https://pypi.org/project/mcp-gateway/">
4+
<img src="https://img.shields.io/pypi/v/mcp-gateway.svg?color=blue" alt="PyPI version">
5+
</a>
6+
<a href="https://pypi.org/project/mcp-gateway/">
7+
<img src="https://img.shields.io/pypi/pyversions/mcp-gateway.svg" alt="Python Versions">
8+
</a>
9+
<a href="./LICENSE">
10+
<img src="https://img.shields.io/github/license/lasso-security/mcp-gateway" alt="License">
11+
</a>
12+
13+
# MCP Gateway
14+
15+
</div>
16+
17+
# Overview
18+
![](docs/MCP_Flow.png)
419

520
MCP Gateway is an advanced intermediary solution for Model Context Protocol (MCP) servers that centralizes and enhances your AI infrastructure.
621

722
MCP Gateway acts as an intermediary between LLMs and other MCP servers. It:
823

9-
1. Reads server configurations from a `mcp.json` file located in your root directory.
10-
2. Manages the lifecycle of configured MCP servers.
11-
3. Intercepts requests and responses to sanitize sensitive information.
12-
4. Provides a unified interface for discovering and interacting with all proxied MCPs.
24+
1. 📄 Reads server configurations from a `mcp.json` file located in your root directory.
25+
2. ⚙️ Manages the lifecycle of configured MCP servers.
26+
3. 🛡️ Intercepts requests and responses to sanitize sensitive information.
27+
4. 🔗 Provides a unified interface for discovering and interacting with all proxied MCPs.
28+
5. 🔒 **Security Scanner** - Analyzes server reputation and security risks before loading MCP servers.
1329

1430
## Installation
1531

@@ -293,6 +309,7 @@ The Lasso guardrail checks content through Lasso's API for security violations b
293309

294310
Read more on our website 👉 [Lasso Security](https://www.lasso.security/).
295311

312+
296313
## Tracing
297314

298315
### Xetrack
@@ -398,6 +415,82 @@ D SELECT server_name,capability_name,path,content_text FROM db.events LIMIT 1;
398415
399416
Of course you can use another MCP server to query the sqlite database 😊
400417
418+
# Scanner
419+
420+
The Security Scanner analyzes MCP servers for potential security risks before loading, providing an additional layer of protection through reputation analysis and tool description scanning.
421+
422+
```bash
423+
mcp-gateway --scan -p basic
424+
```
425+
426+
**Features:**
427+
- 🔍 **Reputation Analysis** - Evaluates server reputation using marketplace (Smithery, NPM) and GitHub data
428+
- 🛡️ **Tool Description Scanning** - Detects hidden instructions, sensitive file patterns, and malicious actions
429+
- ⚡ **Automatic Blocking** - Blocks risky MCPs based on reputation scores (threshold: 30) and security analysis
430+
- 📝 **Configuration Updates** - Automatically updates your MCP configuration file with scan results
431+
432+
## Quickstart
433+
Initial configuration:
434+
```json
435+
{
436+
"mcpServers": {
437+
"mcp-gateway": {
438+
"command": "mcp-gateway",
439+
"args": [
440+
"--mcp-json-path",
441+
"~/.cursor/mcp.json",
442+
"--scan"
443+
],
444+
"servers": {
445+
"filesystem": {
446+
"command": "npx",
447+
"args": [
448+
"-y",
449+
"@modelcontextprotocol/server-filesystem",
450+
"."
451+
]
452+
}
453+
}
454+
}
455+
}
456+
}
457+
```
458+
After the first run, the scanner will analyze all configured MCP servers and add a `blocked` status to your configuration:
459+
```json
460+
{
461+
"mcpServers": {
462+
"mcp-gateway": {
463+
"command": "mcp-gateway",
464+
"args": [
465+
"--mcp-json-path",
466+
"~/.cursor/mcp.json",
467+
"--scan"
468+
],
469+
"servers": {
470+
"filesystem": {
471+
"command": "npx",
472+
"args": [
473+
"-y",
474+
"@modelcontextprotocol/server-filesystem",
475+
"."
476+
],
477+
"blocked": "passed"
478+
}
479+
}
480+
}
481+
}
482+
}
483+
```
484+
**Status Values:**
485+
- `"passed"` - Server passed all security checks and is safe to use
486+
- `"blocked"` - Server failed security checks and will be blocked from loading
487+
- `"skipped"` - Server scanning was skipped (manual override)
488+
- `null` - Server not yet scanned or previously blocked server now considered safe
489+
490+
> **Note:** You can manually change a blocked server to `"skipped"` if you're confident it's safe.
491+
492+
493+
401494
## How It Works
402495
Your agent interacts directly with our MCP Gateway, which functions as a central router and management system. Each underlying MCP is individually wrapped and managed.
403496
@@ -413,6 +506,9 @@ Key Features
413506
* Includes intelligent risk assessment with MCP risk scoring.
414507
* Delivers real-time status monitoring and performance metrics.
415508
509+
**Security Scanner**
510+
* Analyzes MCP server reputation and tool descriptions for security risks before loading.
511+
416512
**Advanced Tracking**
417513
* Maintains detailed logs of all requests and responses for each guardrail.
418514
* Offers cost evaluation tools for MCPs requiring paid tokens.

mcp_gateway/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
This package provides functionality to manage and route requests to multiple MCP servers.
55
"""
66

7-
__version__ = "0.1.0"
7+
__version__ = "1.1.0"

mcp_gateway/security_scanner/config.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66

77
# Configure file logging
88
LOG_DIR = Path(os.path.expanduser("~/.mcp-gateway"))
9-
LOG_DIR.mkdir(parents=True, exist_ok=True) # Ensure the directory exists
9+
LOG_DIR.mkdir(parents=True, exist_ok=True) # Ensure the directory exists
1010
LOG_FILE = LOG_DIR / "scanner.log"
1111

1212
file_handler = logging.FileHandler(LOG_FILE)
13-
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
13+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
1414
file_handler.setFormatter(formatter)
1515
logger.addHandler(file_handler)
1616

1717

18-
1918
class URLS:
2019
SMITHERY = "https://smithery.ai/server/{project_name}"
2120
GITHUB = "https://github.com/{owner}/{project_name}"
@@ -36,9 +35,9 @@ class Keys:
3635

3736
# Smithery specific keys
3837
GITHUB_LINK = "github_link"
39-
VERIFIED = "verified" # Used in both Smithery and GitHub Owner
38+
VERIFIED = "verified" # Used in both Smithery and GitHub Owner
4039
IS_VERIFIED = "is_verified"
41-
LICENSE = "license" # Used in both Smithery and GitHub Repo
40+
LICENSE = "license" # Used in both Smithery and GitHub Repo
4241
MONTHLY_TOOL_USAGE = "monthly_tool_usage"
4342
RUNNING_LOCAL = "running_local"
4443
PUBLISHED_AT = "published_at"
@@ -57,39 +56,55 @@ class Keys:
5756
OWNER_TYPE = "owner_type"
5857
TYPE = "type"
5958
LOCATION = "location"
60-
NAME = "name" # Also used for license fallback
59+
NAME = "name" # Also used for license fallback
6160
TWITTER_USERNAME = "twitter_username"
6261
LOGIN = "login"
63-
62+
6463
# NPM Collector specific keys (some might overlap if semantics are identical)
6564
PACKAGE_NAME = "package_name"
65+
ORIGINAL_PACKAGE_NAME = "original_package_name" # Package name with version tag
66+
VERSION_TAG = "version_tag"
6667
DESCRIPTION = "description"
6768
DOWNLOADS_LAST_MONTH = "downloads_last_month"
6869
NUM_VERSIONS = "num_versions"
6970
DAYS_SINCE_LAST_UPDATED = "days_since_last_updated"
70-
DAYS_SINCE_CREATED = "days_since_created" # Specifically for NPM package creation delta in days
71-
LICENSE_INFO = "license_info" # For the potentially complex license object from NPM
71+
DAYS_SINCE_CREATED = (
72+
"days_since_created" # Specifically for NPM package creation delta in days
73+
)
74+
LICENSE_INFO = "license_info" # For the potentially complex license object from NPM
7275
NUM_MAINTAINERS = "num_maintainers"
7376
MAINTAINERS_NAMES = "maintainers_names"
7477
HOMEPAGE_URL = "homepage_url"
7578
BUG_TRACKER_URL = "bug_tracker_url"
7679
KEYWORDS = "keywords"
77-
ERROR = "error" # For error messages
80+
ERROR = "error" # For error messages
7881

7982
# Score component keys used in ProjectAnalyzer
8083
OWNER_SCORE = "owner_score"
8184
REPO_SCORE = "repo_score"
82-
PROJECT_SCORE = "project_score" # Key for the main project score (e.g., NPM's combined score)
83-
SOURCE_SCORE = "source_score" # Key for a marketplace-specific data score component
85+
PROJECT_SCORE = (
86+
"project_score" # Key for the main project score (e.g., NPM's combined score)
87+
)
88+
SOURCE_SCORE = "source_score" # Key for a marketplace-specific data score component
8489
SMITHERY_SCORE = "smithery_score" # Specific key for Smithery score, also used in the error fallback logic
8590

8691
# Specific Values
8792
ORGANIZATION_OWNER_TYPE = "Organization"
8893

8994

9095
# License Constants
91-
PERMISSIVE_LICENSES = {'mit', 'apache-2.0', 'bsd-3-clause', 'isc'}
92-
93-
TRUSTED_ORGANIZATION_NAMES = ['lasso-security', 'smithery-ai', 'aws', 'google', 'microsoft', 'apple', 'facebook', 'twitter', 'github', 'gitlab', 'bitbucket',
94-
]
95-
96+
PERMISSIVE_LICENSES = {"mit", "apache-2.0", "bsd-3-clause", "isc"}
97+
98+
TRUSTED_ORGANIZATION_NAMES = [
99+
"lasso-security",
100+
"smithery-ai",
101+
"aws",
102+
"google",
103+
"microsoft",
104+
"apple",
105+
"facebook",
106+
"twitter",
107+
"github",
108+
"gitlab",
109+
"bitbucket",
110+
]

mcp_gateway/security_scanner/npm_collector.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,71 @@ def __init__(self, package_name: str) -> None:
2828
Args:
2929
package_name: The name of the npm package to collect data for.
3030
"""
31-
self.package_name: str = package_name.lower()
31+
self.original_package_name: str = package_name
32+
self.package_name: str = self._parse_package_name(package_name)
33+
self.version_tag: Optional[str] = self._extract_version_tag(package_name)
3234
self._raw_data: Optional[Dict[str, Any]] = None
3335
self._download_stats: Optional[Dict[str, Any]] = None
3436

37+
def _parse_package_name(self, package_name: str) -> str:
38+
"""
39+
Parses the package name to remove version tags.
40+
41+
Examples:
42+
- "@upstash/context7-mcp@latest" -> "@upstash/context7-mcp"
43+
- "express@4.18.2" -> "express"
44+
- "@angular/core@16.0.0" -> "@angular/core"
45+
46+
Args:
47+
package_name: The raw package name potentially with version tag
48+
49+
Returns:
50+
The clean package name without version tag
51+
"""
52+
package_name = package_name.strip()
53+
54+
# Handle scoped packages (e.g., @scope/package@version)
55+
if package_name.startswith("@"):
56+
# Find the last @ symbol, which should be the version separator
57+
# We need to be careful because scoped packages already have one @
58+
parts = package_name.split("@")
59+
if len(parts) > 2: # @scope/package@version has 3 parts when split by @
60+
# Reconstruct without the version: @scope/package
61+
return "@" + "@".join(parts[1:-1])
62+
else:
63+
# No version tag, return as-is
64+
return package_name.lower()
65+
else:
66+
# Handle regular packages (e.g., package@version)
67+
if "@" in package_name:
68+
return package_name.split("@")[0].lower()
69+
else:
70+
return package_name.lower()
71+
72+
def _extract_version_tag(self, package_name: str) -> Optional[str]:
73+
"""
74+
Extracts the version tag from a package name.
75+
76+
Args:
77+
package_name: The raw package name potentially with version tag
78+
79+
Returns:
80+
The version tag if present, None otherwise
81+
"""
82+
package_name = package_name.strip()
83+
84+
# Handle scoped packages
85+
if package_name.startswith("@"):
86+
parts = package_name.split("@")
87+
if len(parts) > 2: # @scope/package@version
88+
return parts[-1] # Return the last part as version
89+
else:
90+
# Handle regular packages
91+
if "@" in package_name:
92+
return package_name.split("@")[-1]
93+
94+
return None
95+
3596
def _fetch_json(self, url: str) -> Optional[Dict[str, Any]]:
3697
"""
3798
Synchronously fetches JSON data from a given URL using requests.
@@ -240,6 +301,8 @@ def get_all_data(self) -> Dict[str, Any]:
240301

241302
return {
242303
Keys.PACKAGE_NAME: self.package_name,
304+
Keys.ORIGINAL_PACKAGE_NAME: self.original_package_name,
305+
Keys.VERSION_TAG: self.version_tag,
243306
Keys.DESCRIPTION: self.description,
244307
Keys.GITHUB_LINK: self.github_url,
245308
Keys.DOWNLOADS_LAST_MONTH: self.downloads_last_month,

mcp_gateway/server.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
)
4343
logger = logging.getLogger(__name__)
4444

45+
4546
class Server:
4647
"""Manages the connection and interaction with a single proxied MCP server."""
4748

@@ -66,15 +67,14 @@ def __init__(self, name: str, config: Dict[str, Any]):
6667
self._prompts: List[types.Prompt] = []
6768
logger.info(f"Initialized Proxied Server: {self.name}")
6869

69-
7070
@property
7171
def blocked(self) -> Literal["blocked", "skipped", "passed", None]:
7272
return self.config.get("blocked")
73-
73+
7474
@blocked.setter
7575
def blocked(self, value: Literal["blocked", "skipped", "passed", None]):
7676
self.config["blocked"] = value
77-
77+
7878
@property
7979
def session(self) -> ClientSession:
8080
"""Returns the active ClientSession, raising an error if not started."""
@@ -382,3 +382,16 @@ class GetewayContext:
382382
proxied_servers: Dict[str, Server] = field(default_factory=dict)
383383
plugin_manager: Optional[PluginManager] = None
384384
mcp_json_path: Optional[str] = None
385+
386+
387+
def main():
388+
"""Entry point for backward compatibility - delegates to gateway.main()"""
389+
# Import here to avoid circular imports
390+
from mcp_gateway.gateway import main as gateway_main
391+
392+
logger.info("Starting MCP Gateway server via legacy server.py entry point...")
393+
gateway_main()
394+
395+
396+
if __name__ == "__main__":
397+
main()

pyproject.toml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
11
[project]
22
name = "mcp-gateway"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
description = "A gateway for MCP servers"
55
readme = "README.md"
66
requires-python = ">=3.10"
77
license = "MIT"
8+
classifiers = [
9+
"Development Status :: 4 - Beta",
10+
"Intended Audience :: Developers",
11+
"License :: OSI Approved :: MIT License",
12+
"Programming Language :: Python :: 3",
13+
"Programming Language :: Python :: 3.10",
14+
"Programming Language :: Python :: 3.11",
15+
"Programming Language :: Python :: 3.12",
16+
"Programming Language :: Python :: 3.13",
17+
"Operating System :: OS Independent",
18+
"Topic :: Software Development :: Libraries :: Python Modules",
19+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
20+
"Topic :: System :: Networking",
21+
]
822
dependencies = [
23+
"beautifulsoup4>=4.13.4",
924
"mcp[cli]>=1.6.0",
25+
"requests>=2.32.3",
1026
]
1127

1228
[project.optional-dependencies]

0 commit comments

Comments
 (0)