Skip to content
Merged
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
35 changes: 33 additions & 2 deletions mcpgateway/services/gateway_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,7 @@ async def register_gateway(
existing.display_name = prompt.name
existing.description = prompt.description
existing.template = prompt.template if hasattr(prompt, "template") else ""
existing.argument_schema = self._build_prompt_argument_schema(prompt)
existing.federation_source = gateway.name
existing.modified_by = created_by
existing.modified_from_ip = created_from_ip
Expand All @@ -1074,7 +1075,7 @@ async def register_gateway(
display_name=prompt.name,
description=prompt.description,
template=prompt.template if hasattr(prompt, "template") else "",
argument_schema={}, # Use argument_schema instead of arguments
argument_schema=self._build_prompt_argument_schema(prompt),
# Federation metadata
created_by=created_by or "system",
created_from_ip=created_from_ip,
Expand Down Expand Up @@ -4246,6 +4247,33 @@ def _update_or_create_resources(self, db: Session, resources: List[Any], gateway

return resources_to_add

@staticmethod
def _build_prompt_argument_schema(prompt: Any) -> Dict[str, Any]:
"""Build a JSON-schema-compatible argument_schema dict from a PromptCreate's arguments list.

The MCP protocol's ``prompts/list`` response includes argument metadata
(name, description, required) on each prompt. This helper converts that
list into the internal ``argument_schema`` structure expected by
``DbPrompt`` so that the UI and API can surface the arguments correctly.

Args:
prompt: A PromptCreate (or any object with an ``arguments`` attribute
whose items have ``name``, optional ``description``, and
optional ``required`` fields).

Returns:
Dict with ``type``, ``properties``, and ``required`` keys.
"""
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
for arg in getattr(prompt, "arguments", []) or []:
prop: Dict[str, Any] = {"type": "string"}
if getattr(arg, "description", None):
prop["description"] = arg.description
schema["properties"][arg.name] = prop
if getattr(arg, "required", False):
schema["required"].append(arg.name)
return schema

def _update_or_create_prompts(self, db: Session, prompts: List[Any], gateway: DbGateway, created_via: str, update_visibility: bool = False) -> List[DbPrompt]:
"""Helper to handle update-or-create logic for prompts from MCP server.

Expand Down Expand Up @@ -4286,16 +4314,19 @@ def _update_or_create_prompts(self, db: Session, prompts: List[Any], gateway: Db
# Update existing prompt if there are changes
fields_to_update = False

new_argument_schema = self._build_prompt_argument_schema(prompt)
if (
existing_prompt.description != prompt.description
or existing_prompt.template != (prompt.template if hasattr(prompt, "template") else "")
or (update_visibility and existing_prompt.visibility != gateway.visibility)
or (existing_prompt.argument_schema or {}) != new_argument_schema
):
fields_to_update = True

if fields_to_update:
existing_prompt.description = prompt.description
existing_prompt.template = prompt.template if hasattr(prompt, "template") else ""
existing_prompt.argument_schema = new_argument_schema
if update_visibility:
existing_prompt.visibility = gateway.visibility
logger.debug(f"Updated existing prompt: {prompt.name}")
Expand All @@ -4308,7 +4339,7 @@ def _update_or_create_prompts(self, db: Session, prompts: List[Any], gateway: Db
display_name=prompt.name,
description=prompt.description,
template=prompt.template if hasattr(prompt, "template") else "",
argument_schema={}, # Use argument_schema instead of arguments
argument_schema=self._build_prompt_argument_schema(prompt),
gateway_id=gateway.id,
created_by="system",
created_via=created_via,
Expand Down
13 changes: 7 additions & 6 deletions mcpgateway/static/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5604,11 +5604,12 @@ async function viewPrompt(promptName) {

const argsEl = promptDetailsDiv.querySelector(".prompt-arguments");
if (argsEl) {
argsEl.textContent = JSON.stringify(
prompt.arguments || {},
null,
2,
);
const args = prompt.arguments;
if (!args || args.length === 0) {
argsEl.textContent = "No arguments";
} else {
argsEl.textContent = JSON.stringify(args, null, 2);
}
}

if (prompt.metrics) {
Expand Down Expand Up @@ -5845,7 +5846,7 @@ async function editPrompt(promptId) {

// Validate arguments JSON
const argsValidation = validateJson(
JSON.stringify(prompt.arguments || {}),
JSON.stringify(prompt.arguments || []),
"Arguments",
);
if (argsField && argsValidation.valid) {
Expand Down
173 changes: 171 additions & 2 deletions tests/unit/mcpgateway/services/test_gateway_service_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,174 @@ async def test_update_or_create_resources_existing_resources(self):
assert existing_resource.uri_template == "template_content"
assert existing_resource.visibility == "public"

def test_build_prompt_argument_schema_empty(self):
"""Test _build_prompt_argument_schema returns base schema when no arguments."""
from types import SimpleNamespace

prompt = SimpleNamespace(arguments=[])
schema = GatewayService._build_prompt_argument_schema(prompt)
assert schema == {"type": "object", "properties": {}, "required": []}

def test_build_prompt_argument_schema_with_arguments(self):
"""Test _build_prompt_argument_schema correctly maps MCP argument metadata."""
from types import SimpleNamespace

arg1 = SimpleNamespace(name="name", description="User name", required=True)
arg2 = SimpleNamespace(name="style", description="Greeting style", required=False)
arg3 = SimpleNamespace(name="lang", description=None, required=False)
prompt = SimpleNamespace(arguments=[arg1, arg2, arg3])

schema = GatewayService._build_prompt_argument_schema(prompt)

assert schema["type"] == "object"
assert schema["required"] == ["name"]
assert schema["properties"]["name"] == {"type": "string", "description": "User name"}
assert schema["properties"]["style"] == {"type": "string", "description": "Greeting style"}
# None description should be omitted
assert schema["properties"]["lang"] == {"type": "string"}
assert "lang" not in schema["required"]

def test_build_prompt_argument_schema_no_arguments_attr(self):
"""Test _build_prompt_argument_schema handles missing arguments attribute gracefully."""
from types import SimpleNamespace

prompt = SimpleNamespace() # no 'arguments' attribute
schema = GatewayService._build_prompt_argument_schema(prompt)
assert schema == {"type": "object", "properties": {}, "required": []}

def test_build_prompt_argument_schema_arguments_none(self):
"""Test _build_prompt_argument_schema handles arguments=None gracefully."""
from types import SimpleNamespace

prompt = SimpleNamespace(arguments=None)
schema = GatewayService._build_prompt_argument_schema(prompt)
assert schema == {"type": "object", "properties": {}, "required": []}

@pytest.mark.asyncio
async def test_update_or_create_prompts_new_prompt_with_arguments(self):
"""Test _update_or_create_prompts populates argument_schema from real arguments."""
from types import SimpleNamespace

service = GatewayService()
mock_db = MagicMock()
mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = []
mock_db.execute.return_value = mock_result

mock_gateway = MagicMock()
mock_gateway.id = "gw-1"
mock_gateway.name = "test-gw"
mock_gateway.visibility = "public"
mock_gateway.prompts = []

prompt = SimpleNamespace(
name="greet_user",
description="Greet a user",
template="Hello {name}!",
arguments=[
SimpleNamespace(name="name", description="User name", required=True),
SimpleNamespace(name="style", description="Greeting style", required=False),
],
)

result = service._update_or_create_prompts(mock_db, [prompt], mock_gateway, "test")

assert len(result) == 1
schema = result[0].argument_schema
assert schema["type"] == "object"
assert schema["required"] == ["name"]
assert schema["properties"]["name"] == {"type": "string", "description": "User name"}
assert schema["properties"]["style"] == {"type": "string", "description": "Greeting style"}

@pytest.mark.asyncio
async def test_update_or_create_prompts_argument_schema_change_triggers_update(self):
"""Test that a change in argument_schema alone triggers a prompt update."""
from types import SimpleNamespace

service = GatewayService()
mock_db = MagicMock()

existing_prompt = MagicMock()
existing_prompt.original_name = "greet_user"
existing_prompt.description = "Greet a user"
existing_prompt.template = "Hello {name}!"
existing_prompt.visibility = "public"
existing_prompt.argument_schema = {"type": "object", "properties": {}, "required": []}

mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = [existing_prompt]
mock_db.execute.return_value = mock_result

mock_gateway = MagicMock()
mock_gateway.id = "gw-1"
mock_gateway.visibility = "public"
mock_gateway.prompts = [existing_prompt]

# Same description and template, but different arguments
updated_prompt = SimpleNamespace(
name="greet_user",
description="Greet a user",
template="Hello {name}!",
arguments=[
SimpleNamespace(name="name", description="User name", required=True),
],
)

result = service._update_or_create_prompts(mock_db, [updated_prompt], mock_gateway, "update")

assert len(result) == 0 # No new prompts
expected_schema = {
"type": "object",
"properties": {"name": {"type": "string", "description": "User name"}},
"required": ["name"],
}
assert existing_prompt.argument_schema == expected_schema

@pytest.mark.asyncio
async def test_update_or_create_prompts_no_update_when_schema_unchanged(self):
"""Test that no update is triggered when argument_schema hasn't changed."""
from types import SimpleNamespace

service = GatewayService()
mock_db = MagicMock()

existing_schema = {
"type": "object",
"properties": {"name": {"type": "string", "description": "User name"}},
"required": ["name"],
}
existing_prompt = MagicMock()
existing_prompt.original_name = "greet_user"
existing_prompt.description = "Greet a user"
existing_prompt.template = "Hello {name}!"
existing_prompt.visibility = "public"
existing_prompt.argument_schema = existing_schema

mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = [existing_prompt]
mock_db.execute.return_value = mock_result

mock_gateway = MagicMock()
mock_gateway.id = "gw-1"
mock_gateway.visibility = "public"
mock_gateway.prompts = [existing_prompt]

# Identical description, template, and arguments
same_prompt = SimpleNamespace(
name="greet_user",
description="Greet a user",
template="Hello {name}!",
arguments=[
SimpleNamespace(name="name", description="User name", required=True),
],
)

result = service._update_or_create_prompts(mock_db, [same_prompt], mock_gateway, "update")

assert len(result) == 0
# argument_schema should remain the original object (no assignment happened)
assert existing_prompt.argument_schema is existing_schema

@pytest.mark.asyncio
async def test_update_or_create_prompts_new_prompts(self):
"""Test _update_or_create_prompts creates new prompts."""
Expand Down Expand Up @@ -905,7 +1073,7 @@ async def test_update_or_create_prompts_new_prompts(self):
assert new_prompt.template == "Hello {name}!"
assert new_prompt.created_via == "test"
assert new_prompt.visibility == "private"
assert new_prompt.argument_schema == {}
assert new_prompt.argument_schema == {"type": "object", "properties": {}, "required": []}

@pytest.mark.asyncio
async def test_update_or_create_prompts_existing_prompts(self):
Expand Down Expand Up @@ -953,6 +1121,7 @@ async def test_update_or_create_prompts_existing_prompts(self):
assert existing_prompt.description == "Updated description"
assert existing_prompt.template == "Updated template {var}"
assert existing_prompt.visibility == "public"
assert existing_prompt.argument_schema == {"type": "object", "properties": {}, "required": []}

@pytest.mark.asyncio
async def test_helper_methods_mixed_operations(self):
Expand Down Expand Up @@ -1149,7 +1318,7 @@ async def test_helper_methods_with_metadata_inheritance(self):
prompt = prompts_result[0]
assert prompt.created_via == "metadata_test"
assert prompt.visibility == "team"
assert prompt.argument_schema == {}
assert prompt.argument_schema == {"type": "object", "properties": {}, "required": []}

@pytest.mark.asyncio
async def test_helper_methods_context_propagation(self):
Expand Down
Loading