Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3023,8 +3023,8 @@ PLUGINS_CLI_MARKUP_MODE=rich
# Unsafe URI pattern
# VALIDATION_UNSAFE_URI_PATTERN="[<>\"'\\]"

# MCP tool naming pattern per SEP-986
# VALIDATION_TOOL_NAME_PATTERN="^[a-zA-Z0-9_][a-zA-Z0-9._/-]*$"
# MCP tool naming pattern
# VALIDATION_TOOL_NAME_PATTERN="^[a-zA-Z0-9_.-]+$"

# MCP tool method naming pattern
# VALIDATION_TOOL_METHOD_PATTERN="^[a-zA-Z][a-zA-Z0-9_\\./-]*$"
Expand Down
22 changes: 11 additions & 11 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "(?x)( package-lock\\.json$ |Cargo\\.lock$ |uv\\.lock$ |go\\.sum$ |mcpgateway/sri_hashes\\.json$ )|^.secrets.baseline$",
"lines": null
},
"generated_at": "2026-07-21T16:03:44Z",
"generated_at": "2026-07-22T13:20:53Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -266,63 +266,63 @@
"hashed_secret": "b4673e578b9b30fe8bba1b555b7b59883444c697",
"is_secret": false,
"is_verified": false,
"line_number": 1889,
"line_number": 1891,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "4a0a2df96d4c9a13a282268cab33ac4b8cbb2c72",
"is_secret": false,
"is_verified": false,
"line_number": 1977,
"line_number": 1979,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_secret": false,
"is_verified": false,
"line_number": 2327,
"line_number": 2329,
"type": "Basic Auth Credentials",
"verified_result": null
},
{
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
"is_secret": false,
"is_verified": false,
"line_number": 3692,
"line_number": 3694,
"type": "Basic Auth Credentials",
"verified_result": null
},
{
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
"is_secret": false,
"is_verified": false,
"line_number": 3783,
"line_number": 3785,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "ac371b6dcce28a86c90d12bc57d946a800eebf17",
"is_secret": false,
"is_verified": false,
"line_number": 3826,
"line_number": 3828,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "0b6ec68df700dec4dcd64babd0eda1edccddace1",
"is_secret": false,
"is_verified": false,
"line_number": 3831,
"line_number": 3833,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "4ad6f0082ee224001beb3ca5c3e81c8ceea5ed86",
"is_secret": false,
"is_verified": false,
"line_number": 3836,
"line_number": 3838,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down Expand Up @@ -3838,7 +3838,7 @@
"hashed_secret": "c377074d6473f35a91001981355da793dc808ffd",
"is_secret": false,
"is_verified": false,
"line_number": 749,
"line_number": 746,
"type": "Hex High Entropy String",
"verified_result": null
}
Expand Down Expand Up @@ -3874,7 +3874,7 @@
"hashed_secret": "c377074d6473f35a91001981355da793dc808ffd",
"is_secret": false,
"is_verified": false,
"line_number": 4576,
"line_number": 4580,
"type": "Hex High Entropy String",
"verified_result": null
}
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Release 1.0.6 consolidates **61 PRs** focused on **OAuth RFC 8693 token exchange

### Changed

- **MCP Tool Names and Virtual-Server Aliases** ([#5553](https://github.com/IBM/mcp-context-forge/pull/5553)) - Virtual servers now expose custom tool names and resolve them to upstream names. Tool names follow the MCP 2025-11-25 character set and 128-character limit, with full-string validation that also rejects trailing newlines. **Breaking:** slash-delimited tool names are no longer accepted; rename them to use letters, numbers, periods, underscores, or hyphens.

#### **API**

- **Custom Auth Headers on Tools** ([#5314](https://github.com/IBM/mcp-context-forge/pull/5314)) - `POST /tools` and `PUT /tools/{tool_id}` now correctly persist the `auth_headers` array instead of silently storing `auth_value: null`. Invalid header keys/values are rejected with a 422 rather than an unhandled 500.
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/testing/acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,11 +352,11 @@ MCPGATEWAY_ADMIN_API_ENABLED=true

| Feature | URL | Commands | Expected Result | Status | Notes |
|---------|-----|----------|-----------------|--------|-------|
| XSS in Tool Name | `curl -X POST $GW_URL/tools -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/json" -d '{"name": "<script>alert(1)</script>", "url": "https://example.com"}'` | Attempt XSS | 422 - `"Tool name must start with a letter, number, or underscore and contain only letters, numbers, periods, underscores, hyphens, and slashes"` | ☐ | Input sanitization |
| XSS in Tool Name | `curl -X POST $GW_URL/tools -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/json" -d '{"name": "<script>alert(1)</script>", "url": "https://example.com"}'` | Attempt XSS | 422 - `"Tool name may contain only letters, numbers, underscores, hyphens, and periods"` | ☐ | Input sanitization |
| SQL Injection Pattern | `curl -X POST $GW_URL/admin/tools -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/x-www-form-urlencoded" -d 'name=tool"; DROP TABLE tools; --&url=https://example.com&integrationType=REST&requestType=GET'` | SQL injection | 400/500 - Validation error | ☐ | Pattern blocked |
| Invalid URL Scheme | `curl -X POST $GW_URL/tools -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/json" -d '{"name": "test_tool", "url": "javascript:alert(1)"}'` | JavaScript URL | 422 - `"Tool URL must start with one of: http://, https://, ws://, wss://"` | ☐ | URL validation |
| Directory Traversal | `curl -X POST $GW_URL/admin/resources -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/x-www-form-urlencoded" -d 'uri=../../etc/passwd&name=test&content=data'` | Path traversal | 500 - `"Resource URI cannot contain directory traversal sequences"` | ☐ | Path validation |
| Name Too Long | `curl -X POST $GW_URL/tools -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/json" -d '{"name": "'"$(printf 'a%.0s' {1..300})"'", "url": "https://example.com"}'` | 300 char name | 422 - `"Tool name exceeds maximum length of 255"` | ☐ | Length validation |
| Name Too Long | `curl -X POST $GW_URL/tools -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/json" -d '{"name": "'"$(printf 'a%.0s' {1..300})"'", "url": "https://example.com"}'` | 300 char name | 422 - `"Tool name exceeds MCP spec limit of 128 characters"` | ☐ | Length validation |
| Empty Required Field | `curl -X POST $GW_URL/tools -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/json" -d '{"name": "", "url": "https://example.com"}'` | Empty name | 422 - `"Tool name cannot be empty"` | ☐ | Required validation |
| Whitespace Only | `curl -X POST $GW_URL/tools -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" -H "Content-Type: application/json" -d '{"name": " ", "url": "https://example.com"}'` | Spaces only | 422 - `"Tool name cannot be empty"` | ☐ | Trimming validation |

Expand Down
3 changes: 1 addition & 2 deletions mcpgateway/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18383,8 +18383,7 @@ async def get_observability_traces(
user_email: QueryUserIdentifierNoDescription = None,
name_search: Optional[str] = Query(None, max_length=500),
attribute_search: Optional[str] = Query(None, max_length=500),
# tool_name pattern follows MCP SEP-986 (Specify Format for Tool Names), matching
# mcpgateway.config.Settings.validation_tool_name_pattern. Allows namespacing via '/'.
# tool_name follows mcpgateway.config.Settings.validation_tool_name_pattern.
tool_name: QueryToolName = None,
_user=Depends(get_current_user_with_permissions),
db: Session = Depends(get_db),
Expand Down
26 changes: 26 additions & 0 deletions mcpgateway/cache/registry_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,8 @@ class CacheInvalidationSubscriber:
- registry:{cache_type} - Invalidate registry cache (tools, prompts, etc.)
- tool_lookup:{name} - Invalidate specific tool lookup
- tool_lookup:gateway:{gateway_id} - Invalidate all tools for a gateway
- tool_lookup:server:{server_id} - Invalidate all tools scoped to a virtual server
- tool_lookup:scoped - Invalidate all virtual-server-scoped tool lookups
- admin:{prefix} - Invalidate admin stats cache
- user:{email} - Invalidate auth user cache
- revoke:{jti} - Invalidate auth revocation cache
Expand Down Expand Up @@ -1061,6 +1063,30 @@ async def _process_invalidation(self, message: str, *, channel: str = "") -> Non
cache._cache.pop(key, None) # pyright: ignore[reportPrivateUsage]
logger.debug("CacheInvalidationSubscriber: Cleared local registry:%s cache (%d keys)", cache_type, len(keys_to_remove))

elif message == "tool_lookup:scoped":
# Handle all virtual-server-scoped tool lookup invalidation
# First-Party
from mcpgateway.cache.tool_lookup_cache import tool_lookup_cache # pylint: disable=import-outside-toplevel

with tool_lookup_cache._lock: # pyright: ignore[reportPrivateUsage]
to_remove = [name for name in tool_lookup_cache._cache if name.startswith("server:")] # pyright: ignore[reportPrivateUsage]
for name in to_remove:
tool_lookup_cache._cache.pop(name, None) # pyright: ignore[reportPrivateUsage]
logger.debug("CacheInvalidationSubscriber: Cleared all local scoped tool lookups (%d keys)", len(to_remove))

elif message.startswith("tool_lookup:server:"):
# Handle virtual-server-scoped tool lookup invalidation
server_id = message[len("tool_lookup:server:") :]
# First-Party
from mcpgateway.cache.tool_lookup_cache import tool_lookup_cache # pylint: disable=import-outside-toplevel

key_prefix = tool_lookup_cache._cache_key("", server_id) # pyright: ignore[reportPrivateUsage]
with tool_lookup_cache._lock: # pyright: ignore[reportPrivateUsage]
to_remove = [name for name in tool_lookup_cache._cache if name.startswith(key_prefix)] # pyright: ignore[reportPrivateUsage]
for name in to_remove:
tool_lookup_cache._cache.pop(name, None) # pyright: ignore[reportPrivateUsage]
logger.debug("CacheInvalidationSubscriber: Cleared local tool_lookup for server %s (%d keys)", server_id, len(to_remove))

elif message.startswith("tool_lookup:gateway:"):
# Handle gateway-wide tool lookup invalidation
gateway_id = message[len("tool_lookup:gateway:") :]
Expand Down
Loading