Skip to content

Commit 892c9ee

Browse files
authored
Fix SSE Tool Invocation Fails After Integration Type Migration post PR IBM#676 - closes IBM#696 (IBM#697)
Signed-off-by: Mihai Criveti <[email protected]>
1 parent 7b3c849 commit 892c9ee

File tree

5 files changed

+110
-42
lines changed

5 files changed

+110
-42
lines changed

mcpgateway/schemas.py

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,11 @@ class AuthenticationValues(BaseModelWithConfigDict):
278278
auth_value: Optional[str] = Field(None, description="Encoded Authentication values")
279279

280280
# Only For tool read and view tool
281-
username: str = Field("", description="Username for basic authentication")
282-
password: str = Field("", description="Password for basic authentication")
283-
token: str = Field("", description="Bearer token for authentication")
284-
auth_header_key: str = Field("", description="Key for custom headers authentication")
285-
auth_header_value: str = Field("", description="Value for custom headers authentication")
281+
username: Optional[str] = Field("", description="Username for basic authentication")
282+
password: Optional[str] = Field("", description="Password for basic authentication")
283+
token: Optional[str] = Field("", description="Bearer token for authentication")
284+
auth_header_key: Optional[str] = Field("", description="Key for custom headers authentication")
285+
auth_header_value: Optional[str] = Field("", description="Value for custom headers authentication")
286286

287287

288288
class ToolCreate(BaseModel):
@@ -294,7 +294,7 @@ class ToolCreate(BaseModel):
294294
name (str): Unique name for the tool.
295295
url (Union[str, AnyHttpUrl]): Tool endpoint URL.
296296
description (Optional[str]): Tool description.
297-
integration_type (Literal["REST"]): Tool integration type for REST integrations.
297+
integration_type (Literal["REST", "MCP"]): Tool integration type - REST for individual endpoints, MCP for gateway-discovered tools.
298298
request_type (Literal["GET", "POST", "PUT", "DELETE", "PATCH"]): HTTP method to be used for invoking the tool.
299299
headers (Optional[Dict[str, str]]): Additional headers to send when invoking the tool.
300300
input_schema (Optional[Dict[str, Any]]): JSON Schema for validating tool parameters. Alias 'inputSchema'.
@@ -309,7 +309,7 @@ class ToolCreate(BaseModel):
309309
name: str = Field(..., description="Unique name for the tool")
310310
url: Union[str, AnyHttpUrl] = Field(None, description="Tool endpoint URL")
311311
description: Optional[str] = Field(None, description="Tool description")
312-
integration_type: Literal["REST"] = Field("REST", description="'REST' for REST integrations")
312+
integration_type: Literal["REST", "MCP"] = Field("REST", description="'REST' for individual endpoints, 'MCP' for gateway-discovered tools")
313313
request_type: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "SSE", "STDIO", "STREAMABLEHTTP"] = Field("SSE", description="HTTP method to be used for invoking the tool")
314314
headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
315315
input_schema: Optional[Dict[str, Any]] = Field(default_factory=lambda: {"type": "object", "properties": {}}, description="JSON Schema for validating tool parameters", alias="inputSchema")
@@ -457,12 +457,16 @@ def validate_request_type(cls, v: str, info: ValidationInfo) -> str:
457457
>>> info = type('obj', (object,), {'data': {'integration_type': 'REST'}})
458458
>>> ToolCreate.validate_request_type('GET', info)
459459
'GET'
460-
>>> ToolCreate.validate_request_type('POST', info)
461-
'POST'
460+
461+
>>> # Test MCP integration types with valid transport
462+
>>> info = type('obj', (object,), {'data': {'integration_type': 'MCP'}})
463+
>>> ToolCreate.validate_request_type('SSE', info)
464+
'SSE'
462465
463466
>>> # Test invalid REST type
467+
>>> info_rest = type('obj', (object,), {'data': {'integration_type': 'REST'}})
464468
>>> try:
465-
... ToolCreate.validate_request_type('SSE', info)
469+
... ToolCreate.validate_request_type('SSE', info_rest)
466470
... except ValueError as e:
467471
... "not allowed for REST" in str(e)
468472
True
@@ -478,12 +482,17 @@ def validate_request_type(cls, v: str, info: ValidationInfo) -> str:
478482

479483
integration_type = info.data.get("integration_type")
480484

481-
if integration_type != "REST":
485+
if integration_type not in ["REST", "MCP"]:
482486
raise ValueError(f"Unknown integration type: {integration_type}")
483487

484-
allowed = ["GET", "POST", "PUT", "DELETE", "PATCH"]
485-
if v not in allowed:
486-
raise ValueError(f"Request type '{v}' not allowed for REST. Only {allowed} methods are accepted.")
488+
if integration_type == "REST":
489+
allowed = ["GET", "POST", "PUT", "DELETE", "PATCH"]
490+
if v not in allowed:
491+
raise ValueError(f"Request type '{v}' not allowed for REST. Only {allowed} methods are accepted.")
492+
elif integration_type == "MCP":
493+
allowed = ["SSE", "STDIO", "STREAMABLEHTTP"]
494+
if v not in allowed:
495+
raise ValueError(f"Request type '{v}' not allowed for MCP. Only {allowed} transports are accepted.")
487496

488497
return v
489498

@@ -551,8 +560,37 @@ def assemble_auth(cls, values: Dict[str, Any]) -> Dict[str, Any]:
551560
encoded_auth = encode_auth({"Authorization": f"Bearer {values.get('auth_token', '')}"})
552561
values["auth"] = {"auth_type": "bearer", "auth_value": encoded_auth}
553562
elif auth_type.lower() == "authheaders":
554-
encoded_auth = encode_auth({values.get("auth_header_key", ""): values.get("auth_header_value", "")})
555-
values["auth"] = {"auth_type": "authheaders", "auth_value": encoded_auth}
563+
header_key = values.get("auth_header_key", "")
564+
header_value = values.get("auth_header_value", "")
565+
if header_key and header_value:
566+
encoded_auth = encode_auth({header_key: header_value})
567+
values["auth"] = {"auth_type": "authheaders", "auth_value": encoded_auth}
568+
else:
569+
# Don't encode empty headers - leave auth empty
570+
values["auth"] = {"auth_type": "authheaders", "auth_value": None}
571+
return values
572+
573+
@model_validator(mode="before")
574+
@classmethod
575+
def prevent_manual_mcp_creation(cls, values: Dict[str, Any]) -> Dict[str, Any]:
576+
"""
577+
Prevent manual creation of MCP tools via API.
578+
579+
MCP tools should only be created by the gateway service when discovering
580+
tools from MCP servers. Users should add MCP servers via the Gateways interface.
581+
582+
Args:
583+
values: The input values
584+
585+
Returns:
586+
Dict[str, Any]: The validated values
587+
588+
Raises:
589+
ValueError: If attempting to manually create MCP integration type
590+
"""
591+
integration_type = values.get("integration_type")
592+
if integration_type == "MCP":
593+
raise ValueError("Cannot manually create MCP tools. Add MCP servers via the Gateways interface - " "tools will be auto-discovered and registered with integration_type='MCP'.")
556594
return values
557595

558596

@@ -565,7 +603,7 @@ class ToolUpdate(BaseModelWithConfigDict):
565603
name: Optional[str] = Field(None, description="Unique name for the tool")
566604
url: Optional[Union[str, AnyHttpUrl]] = Field(None, description="Tool endpoint URL")
567605
description: Optional[str] = Field(None, description="Tool description")
568-
integration_type: Optional[Literal["REST"]] = Field(None, description="Tool integration type")
606+
integration_type: Optional[Literal["REST", "MCP"]] = Field(None, description="Tool integration type")
569607
request_type: Optional[Literal["GET", "POST", "PUT", "DELETE", "PATCH"]] = Field(None, description="HTTP method to be used for invoking the tool")
570608
headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
571609
input_schema: Optional[Dict[str, Any]] = Field(None, description="JSON Schema for validating tool parameters")
@@ -678,7 +716,13 @@ def validate_request_type(cls, v: str, values: Dict[str, Any]) -> str:
678716
"""
679717

680718
integration_type = values.data.get("integration_type", "REST")
681-
allowed = ["GET", "POST", "PUT", "DELETE", "PATCH"]
719+
720+
if integration_type == "REST":
721+
allowed = ["GET", "POST", "PUT", "DELETE", "PATCH"]
722+
elif integration_type == "MCP":
723+
allowed = ["SSE", "STDIO", "STREAMABLEHTTP"]
724+
else:
725+
raise ValueError(f"Unknown integration type: {integration_type}")
682726

683727
if v not in allowed:
684728
raise ValueError(f"Request type '{v}' not allowed for {integration_type} integration")
@@ -721,8 +765,37 @@ def assemble_auth(cls, values: Dict[str, Any]) -> Dict[str, Any]:
721765
encoded_auth = encode_auth({"Authorization": f"Bearer {values.get('auth_token', '')}"})
722766
values["auth"] = {"auth_type": "bearer", "auth_value": encoded_auth}
723767
elif auth_type.lower() == "authheaders":
724-
encoded_auth = encode_auth({values.get("auth_header_key", ""): values.get("auth_header_value", "")})
725-
values["auth"] = {"auth_type": "authheaders", "auth_value": encoded_auth}
768+
header_key = values.get("auth_header_key", "")
769+
header_value = values.get("auth_header_value", "")
770+
if header_key and header_value:
771+
encoded_auth = encode_auth({header_key: header_value})
772+
values["auth"] = {"auth_type": "authheaders", "auth_value": encoded_auth}
773+
else:
774+
# Don't encode empty headers - leave auth empty
775+
values["auth"] = {"auth_type": "authheaders", "auth_value": None}
776+
return values
777+
778+
@model_validator(mode="before")
779+
@classmethod
780+
def prevent_manual_mcp_update(cls, values: Dict[str, Any]) -> Dict[str, Any]:
781+
"""
782+
Prevent updating tools to MCP integration type via API.
783+
784+
MCP tools should only be managed by the gateway service. Users should not
785+
be able to change a REST tool to MCP type or vice versa manually.
786+
787+
Args:
788+
values: The input values
789+
790+
Returns:
791+
Dict[str, Any]: The validated values
792+
793+
Raises:
794+
ValueError: If attempting to update to MCP integration type
795+
"""
796+
integration_type = values.get("integration_type")
797+
if integration_type == "MCP":
798+
raise ValueError("Cannot update tools to MCP integration type. MCP tools are managed by the gateway service.")
726799
return values
727800

728801

mcpgateway/services/gateway_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ async def register_gateway(self, db: Session, gateway: GatewayCreate) -> Gateway
409409
original_name_slug=slugify(tool.name),
410410
url=gateway.url,
411411
description=tool.description,
412-
integration_type=tool.integration_type,
412+
integration_type="MCP", # Gateway-discovered tools are MCP type
413413
request_type=tool.request_type,
414414
headers=tool.headers,
415415
input_schema=tool.input_schema,
@@ -611,7 +611,7 @@ async def update_gateway(self, db: Session, gateway_id: str, gateway_update: Gat
611611
original_name_slug=slugify(tool.name),
612612
url=gateway.url,
613613
description=tool.description,
614-
integration_type=tool.integration_type,
614+
integration_type="MCP", # Gateway-discovered tools are MCP type
615615
request_type=tool.request_type,
616616
headers=tool.headers,
617617
input_schema=tool.input_schema,
@@ -765,7 +765,7 @@ async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bo
765765
original_name_slug=slugify(tool.name),
766766
url=gateway.url,
767767
description=tool.description,
768-
integration_type=tool.integration_type,
768+
integration_type="MCP", # Gateway-discovered tools are MCP type
769769
request_type=tool.request_type,
770770
headers=tool.headers,
771771
input_schema=tool.input_schema,

mcpgateway/services/tool_service.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -650,11 +650,14 @@ async def invoke_tool(self, db: Session, name: str, arguments: Dict[str, Any], r
650650
headers = tool.headers or {}
651651
if tool.integration_type == "REST":
652652
credentials = decode_auth(tool.auth_value)
653-
headers.update(credentials)
653+
# Filter out empty header names/values to avoid "Illegal header name" errors
654+
filtered_credentials = {k: v for k, v in credentials.items() if k and v}
655+
headers.update(filtered_credentials)
654656

655657
# Only call get_passthrough_headers if we actually have request headers to pass through
656658
if request_headers:
657659
headers = get_passthrough_headers(request_headers, headers, db)
660+
658661
# Build the payload based on integration type
659662
payload = arguments.copy()
660663

@@ -698,45 +701,38 @@ async def invoke_tool(self, db: Session, name: str, arguments: Dict[str, Any], r
698701
elif tool.integration_type == "MCP":
699702
transport = tool.request_type.lower()
700703
gateway = db.execute(select(DbGateway).where(DbGateway.id == tool.gateway_id).where(DbGateway.enabled)).scalar_one_or_none()
701-
headers = decode_auth(gateway.auth_value)
704+
headers = decode_auth(gateway.auth_value if gateway else None)
702705

703706
# Get combined headers including gateway auth and passthrough
704-
# base_headers = decode_auth(gateway.auth_value) if gateway and gateway.auth_value else {}
705707
if request_headers:
706708
headers = get_passthrough_headers(request_headers, headers, db, gateway)
707709

708-
async def connect_to_sse_server(server_url: str) -> str:
709-
"""
710-
Connect to an MCP server running with SSE transport
710+
async def connect_to_sse_server(server_url: str):
711+
"""Connect to an MCP server running with SSE transport.
711712
712713
Args:
713-
server_url (str): MCP Server SSE URL
714+
server_url: MCP Server SSE URL
714715
715716
Returns:
716-
str: Result of tool call
717+
ToolResult: Result of tool call
717718
"""
718-
# Use async with directly to manage the context
719719
async with sse_client(url=server_url, headers=headers) as streams:
720720
async with ClientSession(*streams) as session:
721-
# Initialize the session
722721
await session.initialize()
723722
tool_call_result = await session.call_tool(tool.original_name, arguments)
724723
return tool_call_result
725724

726-
async def connect_to_streamablehttp_server(server_url: str) -> str:
727-
"""
728-
Connect to an MCP server running with Streamable HTTP transport
725+
async def connect_to_streamablehttp_server(server_url: str):
726+
"""Connect to an MCP server running with Streamable HTTP transport.
729727
730728
Args:
731-
server_url (str): MCP Server URL
729+
server_url: MCP Server URL
732730
733731
Returns:
734-
str: Result of tool call
732+
ToolResult: Result of tool call
735733
"""
736-
# Use async with directly to manage the context
737734
async with streamablehttp_client(url=server_url, headers=headers) as (read_stream, write_stream, _get_session_id):
738735
async with ClientSession(read_stream, write_stream) as session:
739-
# Initialize the session
740736
await session.initialize()
741737
tool_call_result = await session.call_tool(tool.original_name, arguments)
742738
return tool_call_result

mcpgateway/static/admin.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3144,7 +3144,6 @@ function createParameterForm(parameterCount) {
31443144
// ===================================================================
31453145

31463146
const integrationRequestMap = {
3147-
MCP: ["SSE", "STREAMABLEHTTP", "STDIO"],
31483147
REST: ["GET", "POST", "PUT", "PATCH", "DELETE"],
31493148
};
31503149

mcpgateway/templates/admin.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1173,7 +1173,7 @@ <h3 class="text-lg font-bold mb-4 dark:text-gray-200">
11731173
<div class="mt-6">
11741174
<button
11751175
type="submit"
1176-
x-tooltip="'💡 Registers a new Tool from an existing REST or MCP endpoint.'"
1176+
x-tooltip="'💡 Registers a new Tool from an existing REST endpoint. Use Gateways to add MCP servers.'"
11771177
class="inline-flex justify-center py-2 px-4 border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
11781178
>
11791179
Add Tool

0 commit comments

Comments
 (0)