Skip to content

Commit 0d9371c

Browse files
authored
Merge pull request #53 from dheerajoruganty/feature/latest-changes
FGAC For MCPGW's Intelligent tool finder
2 parents b3337db + dea0faa commit 0d9371c

File tree

8 files changed

+1807
-174
lines changed

8 files changed

+1807
-174
lines changed

agents/agent.py

Lines changed: 169 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import mcp
5555
from mcp import ClientSession
5656
from mcp.client.sse import sse_client
57+
import httpx
58+
import re
5759

5860
# Import dotenv for loading environment variables
5961
try:
@@ -334,15 +336,22 @@ async def invoke_mcp_tool(mcp_registry_url: str, server_name: str, tool_name: st
334336
# Construct the MCP server URL from the registry URL and server name using standard URL parsing
335337
parsed_url = urlparse(mcp_registry_url)
336338

337-
# Extract the scheme and netloc (hostname:port) from the parsed URL
339+
# Extract the scheme, netloc and path from the parsed URL
338340
scheme = parsed_url.scheme
339341
netloc = parsed_url.netloc
342+
path = parsed_url.path
343+
344+
# If the path ends with '/sse', remove it to get the base path
345+
if path.endswith('/sse'):
346+
base_path = path[:-4] # Remove '/sse' from the end
347+
else:
348+
base_path = path
340349

341-
# Construct the base URL with scheme and netloc
342-
base_url = f"{scheme}://{netloc}"
350+
# Construct the base URL with scheme, netloc and base path
351+
base_url = f"{scheme}://{netloc}{base_path}"
343352

344353
# Create the server URL by joining the base URL with the server name and sse path
345-
server_url = urljoin(base_url, f"{server_name}/sse")
354+
server_url = urljoin(base_url + '/', f"{server_name}/sse")
346355
print(f"Server URL: {server_url}")
347356

348357
# Prepare headers based on authentication method
@@ -399,6 +408,60 @@ def redact_sensitive_value(value: str, show_chars: int = 4) -> str:
399408
return "*" * len(value) if value else ""
400409
return value[:show_chars] + "*" * (len(value) - show_chars)
401410

411+
def normalize_sse_endpoint_url_for_request(url_str: str, original_sse_url: str) -> str:
412+
"""
413+
Normalize URLs in HTTP requests by preserving mount paths for non-mounted servers.
414+
415+
This function only applies fixes when the request is for the same server as the original SSE URL.
416+
It should NOT modify requests to different servers (like currenttime, fininfo, etc.)
417+
418+
Example:
419+
- Original SSE: http://localhost/mcpgw2/sse
420+
- Request to same server: http://localhost/messages/?session_id=123 -> http://localhost/mcpgw2/messages/?session_id=123
421+
- Request to different server: http://localhost/currenttime/messages/?session_id=123 -> unchanged (already correct)
422+
"""
423+
if '/messages/' not in url_str:
424+
return url_str
425+
426+
# Parse the original SSE URL to extract the base path
427+
from urllib.parse import urlparse
428+
parsed_original = urlparse(original_sse_url)
429+
parsed_current = urlparse(url_str)
430+
431+
# Only apply fixes if this is the same host/port as the original SSE URL
432+
if parsed_current.netloc != parsed_original.netloc:
433+
return url_str
434+
435+
original_path = parsed_original.path
436+
437+
# Remove /sse from the original path to get the base mount path
438+
if original_path.endswith('/sse'):
439+
base_mount_path = original_path[:-4] # Remove '/sse'
440+
else:
441+
base_mount_path = original_path
442+
443+
# Only apply the fix if:
444+
# 1. There is a base mount path (non-empty)
445+
# 2. The current path is exactly /messages/... (indicating it's missing the mount path)
446+
# 3. The current path doesn't already contain a mount path
447+
if (base_mount_path and
448+
parsed_current.path.startswith('/messages/') and
449+
not parsed_current.path.startswith(base_mount_path)):
450+
451+
# The mount path is missing, we need to add it back
452+
# Reconstruct the URL with the mount path
453+
new_path = base_mount_path + parsed_current.path
454+
fixed_url = f"{parsed_current.scheme}://{parsed_current.netloc}{new_path}"
455+
if parsed_current.query:
456+
fixed_url += f"?{parsed_current.query}"
457+
if parsed_current.fragment:
458+
fixed_url += f"#{parsed_current.fragment}"
459+
460+
logger.debug(f"Fixed mount path in request URL: {url_str} -> {fixed_url}")
461+
return fixed_url
462+
463+
return url_str
464+
402465
def load_system_prompt():
403466
"""
404467
Load the system prompt template from the system_prompt.txt file.
@@ -593,89 +656,117 @@ async def main():
593656
redacted_headers[k] = v
594657
logger.info(f"Using authentication headers: {redacted_headers}")
595658

596-
# Initialize MCP client with the server configuration and authentication headers
597-
client = MultiServerMCPClient(
598-
{
599-
"mcp_registry": {
600-
"url": server_url,
601-
"transport": "sse",
602-
"headers": auth_headers
603-
}
604-
}
605-
)
606-
logger.info("Connected to MCP server successfully with authentication")
607-
608-
# Get available tools from MCP and display them
609-
mcp_tools = await client.get_tools()
610-
logger.info(f"Available MCP tools: {[tool.name for tool in mcp_tools]}")
611-
612-
# Add the calculator and invoke_mcp_tool to the tools array
613-
# The invoke_mcp_tool function already supports authentication parameters
614-
all_tools = [calculator, invoke_mcp_tool] + mcp_tools
615-
logger.info(f"All available tools: {[tool.name if hasattr(tool, 'name') else tool.__name__ for tool in all_tools]}")
659+
# Apply monkey patch to fix mount path issues in httpx requests
660+
# This fixes the issue where non-mounted servers with default paths lose their mount path
661+
# in POST requests to /messages/ endpoints
662+
original_request = httpx.AsyncClient.request
616663

617-
# Create the agent with the model and all tools
618-
agent = create_react_agent(
619-
model,
620-
all_tools
621-
)
664+
async def patched_request(self, method, url, **kwargs):
665+
# Fix mount path issues in requests
666+
if isinstance(url, str) and '/messages/' in url:
667+
url = normalize_sse_endpoint_url_for_request(url, server_url)
668+
elif hasattr(url, '__str__') and '/messages/' in str(url):
669+
url = normalize_sse_endpoint_url_for_request(str(url), server_url)
670+
return await original_request(self, method, url, **kwargs)
622671

623-
# Load and format the system prompt with the current time and MCP registry URL
624-
system_prompt_template = load_system_prompt()
672+
# Apply the patch
673+
httpx.AsyncClient.request = patched_request
674+
logger.info("Applied httpx monkey patch to fix mount path issues")
625675

626-
# Prepare authentication parameters for system prompt
627-
if args.use_session_cookie:
628-
system_prompt = system_prompt_template.format(
629-
current_utc_time=current_utc_time,
630-
mcp_registry_url=args.mcp_registry_url,
631-
auth_token='', # Not used for session cookie auth
632-
user_pool_id=args.user_pool_id or '',
633-
client_id=args.client_id or '',
634-
region=args.region or 'us-east-1',
635-
auth_method=auth_method,
636-
session_cookie=session_cookie
676+
try:
677+
# Initialize MCP client with the server configuration and authentication headers
678+
client = MultiServerMCPClient(
679+
{
680+
"mcp_registry": {
681+
"url": server_url,
682+
"transport": "sse",
683+
"headers": auth_headers
684+
}
685+
}
637686
)
638-
else:
639-
system_prompt = system_prompt_template.format(
640-
current_utc_time=current_utc_time,
641-
mcp_registry_url=args.mcp_registry_url,
642-
auth_token=access_token,
643-
user_pool_id=args.user_pool_id,
644-
client_id=args.client_id,
645-
region=args.region,
646-
auth_method=auth_method,
647-
session_cookie='' # Not used for M2M auth
687+
logger.info("Connected to MCP server successfully with authentication, server_url: " + server_url)
688+
689+
# Get available tools from MCP and display them
690+
mcp_tools = await client.get_tools()
691+
logger.info(f"Available MCP tools: {[tool.name for tool in mcp_tools]}")
692+
693+
# Add the calculator and invoke_mcp_tool to the tools array
694+
# The invoke_mcp_tool function already supports authentication parameters
695+
all_tools = [calculator, invoke_mcp_tool] + mcp_tools
696+
logger.info(f"All available tools: {[tool.name if hasattr(tool, 'name') else tool.__name__ for tool in all_tools]}")
697+
698+
# Create the agent with the model and all tools
699+
agent = create_react_agent(
700+
model,
701+
all_tools
648702
)
649-
650-
# Format the message with system message first
651-
formatted_messages = [
652-
{"role": "system", "content": system_prompt},
653-
{"role": "user", "content": args.message}
654-
]
655-
656-
logger.info("\nInvoking agent...\n" + "-"*40)
657-
658-
# Invoke the agent with the formatted messages
659-
response = await agent.ainvoke({"messages": formatted_messages})
660-
661-
logger.info("\nResponse:" + "\n" + "-"*40)
662-
#print(response)
663-
print_agent_response(response)
664-
665-
# Process and display the response
666-
if response and "messages" in response and response["messages"]:
667-
# Get the last message from the response
668-
last_message = response["messages"][-1]
669703

670-
if isinstance(last_message, dict) and "content" in last_message:
671-
# Display the content of the response
672-
print(last_message["content"])
704+
# Load and format the system prompt with the current time and MCP registry URL
705+
system_prompt_template = load_system_prompt()
706+
707+
# Prepare authentication parameters for system prompt
708+
if args.use_session_cookie:
709+
system_prompt = system_prompt_template.format(
710+
current_utc_time=current_utc_time,
711+
mcp_registry_url=args.mcp_registry_url,
712+
auth_token='', # Not used for session cookie auth
713+
user_pool_id=args.user_pool_id or '',
714+
client_id=args.client_id or '',
715+
region=args.region or 'us-east-1',
716+
auth_method=auth_method,
717+
session_cookie=session_cookie
718+
)
673719
else:
674-
print(str(last_message.content))
675-
else:
676-
print("No valid response received")
720+
system_prompt = system_prompt_template.format(
721+
current_utc_time=current_utc_time,
722+
mcp_registry_url=args.mcp_registry_url,
723+
auth_token=access_token,
724+
user_pool_id=args.user_pool_id,
725+
client_id=args.client_id,
726+
region=args.region,
727+
auth_method=auth_method,
728+
session_cookie='' # Not used for M2M auth
729+
)
730+
731+
# Format the message with system message first
732+
formatted_messages = [
733+
{"role": "system", "content": system_prompt},
734+
{"role": "user", "content": args.message}
735+
]
736+
737+
logger.info("\nInvoking agent...\n" + "-"*40)
738+
739+
# Invoke the agent with the formatted messages
740+
response = await agent.ainvoke({"messages": formatted_messages})
741+
742+
logger.info("\nResponse:" + "\n" + "-"*40)
743+
#print(response)
744+
print_agent_response(response)
745+
746+
# Process and display the response
747+
if response and "messages" in response and response["messages"]:
748+
# Get the last message from the response
749+
last_message = response["messages"][-1]
750+
751+
if isinstance(last_message, dict) and "content" in last_message:
752+
# Display the content of the response
753+
print(last_message["content"])
754+
else:
755+
print(str(last_message.content))
756+
else:
757+
print("No valid response received")
758+
759+
finally:
760+
# Restore original httpx behavior
761+
httpx.AsyncClient.request = original_request
762+
logger.info("Restored original httpx behavior")
677763

678764
except Exception as e:
765+
# Restore original httpx behavior in case of error
766+
try:
767+
httpx.AsyncClient.request = original_request
768+
except NameError:
769+
pass # original_request might not be defined if error occurred before monkey patch
679770
print(f"Error: {str(e)}")
680771
import traceback
681772
print(traceback.format_exc())

auth_server/server.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ async def validate_request(request: Request):
725725

726726
# Validate scope-based access if we have server/tool information
727727
user_scopes = validation_result.get('scopes', [])
728-
if request_payload and server_name and tool_name and user_scopes:
728+
if request_payload and server_name and tool_name:
729729
# Extract method and actual tool name
730730
method = tool_name # The extracted tool_name is actually the method
731731
actual_tool_name = None
@@ -737,6 +737,15 @@ async def validate_request(request: Request):
737737
actual_tool_name = params.get('name')
738738
logger.info(f"Extracted actual tool name for tools/call: '{actual_tool_name}'")
739739

740+
# Check if user has any scopes - if not, deny access (fail closed)
741+
if not user_scopes:
742+
logger.warning(f"Access denied for user {validation_result.get('username')} to {server_name}.{method} (tool: {actual_tool_name}) - no scopes configured")
743+
raise HTTPException(
744+
status_code=403,
745+
detail=f"Access denied to {server_name}.{method} - user has no scopes configured",
746+
headers={"Connection": "close"}
747+
)
748+
740749
if not validate_server_tool_access(server_name, method, actual_tool_name, user_scopes):
741750
logger.warning(f"Access denied for user {validation_result.get('username')} to {server_name}.{method} (tool: {actual_tool_name})")
742751
raise HTTPException(

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ services:
2727
- /opt/mcp-gateway/models:/app/registry/models
2828
- /home/ubuntu/ssl_data:/etc/ssl
2929
- /var/log/mcp-gateway:/app/logs
30+
- /opt/mcp-gateway/auth_server/scopes.yml:/app/auth_server/scopes.yml
3031
depends_on:
3132
- auth-server
3233
restart: unless-stopped
@@ -49,6 +50,7 @@ services:
4950
- "8888:8888"
5051
volumes:
5152
- /var/log/mcp-gateway:/app/logs
53+
- /opt/mcp-gateway/auth_server/scopes.yml:/app/auth_server/scopes.yml
5254
restart: unless-stopped
5355

5456
# Current Time MCP Server
@@ -93,6 +95,7 @@ services:
9395
volumes:
9496
- /opt/mcp-gateway/servers:/app/registry/servers
9597
- /opt/mcp-gateway/models:/app/registry/models
98+
- /opt/mcp-gateway/auth_server/scopes.yml:/app/auth_server/scopes.yml
9699
ports:
97100
- "8003:8003"
98101
depends_on:

registry/api/server_routes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ def can_perform_action(permission: str, service_name: str) -> bool:
6565

6666
# Filter services based on UI permissions
6767
accessible_services = user_context.get('accessible_services', [])
68+
logger.info(f"DEBUG: User {user_context['username']} accessible_services: {accessible_services}")
69+
logger.info(f"DEBUG: User {user_context['username']} ui_permissions: {user_context.get('ui_permissions', {})}")
70+
logger.info(f"DEBUG: User {user_context['username']} scopes: {user_context.get('scopes', [])}")
6871

6972
for path in sorted_server_paths:
7073
server_info = all_servers[path]

registry/core/nginx_service.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ async def generate_config_async(self, servers: Dict[str, Dict[str, Any]]) -> boo
118118
# Authenticate request - pass entire request to auth server
119119
auth_request /validate;
120120
121+
# Capture auth server response headers for forwarding
122+
auth_request_set $auth_user $upstream_http_x_user;
123+
auth_request_set $auth_username $upstream_http_x_username;
124+
auth_request_set $auth_client_id $upstream_http_x_client_id;
125+
auth_request_set $auth_scopes $upstream_http_x_scopes;
126+
auth_request_set $auth_method $upstream_http_x_auth_method;
127+
auth_request_set $auth_server_name $upstream_http_x_server_name;
128+
auth_request_set $auth_tool_name $upstream_http_x_tool_name;
129+
121130
# Proxy to MCP server
122131
proxy_pass {proxy_pass_url};
123132
proxy_http_version 1.1;
@@ -135,6 +144,15 @@ async def generate_config_async(self, servers: Dict[str, Dict[str, Any]]) -> boo
135144
proxy_set_header X-Client-Id $http_x_client_id;
136145
proxy_set_header X-Region $http_x_region;
137146
147+
# Forward auth server response headers to backend
148+
proxy_set_header X-User $auth_user;
149+
proxy_set_header X-Username $auth_username;
150+
proxy_set_header X-Client-Id-Auth $auth_client_id;
151+
proxy_set_header X-Scopes $auth_scopes;
152+
proxy_set_header X-Auth-Method $auth_method;
153+
proxy_set_header X-Server-Name $auth_server_name;
154+
proxy_set_header X-Tool-Name $auth_tool_name;
155+
138156
# For SSE connections and WebSocket upgrades
139157
proxy_buffering off;
140158
proxy_cache off;

servers/mcpgw/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "MCP server to interact with the MCP Gateway Registry API" # Updat
55
readme = "README.md"
66
requires-python = ">=3.12,<3.13"
77
dependencies = [
8-
"mcp>=1.9.3",
8+
"fastmcp>=2.0.0", # Updated to FastMCP 2.0
99
"pydantic>=2.11.3",
1010
"httpx>=0.27.0", # Added httpx
1111
"python-dotenv>=1.0.0", # Added dotenv as it's used in server.py

0 commit comments

Comments
 (0)