Jeremiah Lowin commited on
Commit
71d1f33
·
1 Parent(s): 6ad13fe

Ensure openapi descriptions are included in tool details

Browse files
src/fastmcp/server/openapi.py CHANGED
@@ -534,10 +534,12 @@ class FastMCPOpenAPI(FastMCP):
534
  or f"Executes {route.method} {route.path}"
535
  )
536
 
537
- # Format enhanced description
538
  enhanced_description = format_description_with_responses(
539
  base_description=base_description,
540
  responses=route.responses,
 
 
541
  )
542
 
543
  tool = OpenAPITool(
@@ -565,10 +567,12 @@ class FastMCPOpenAPI(FastMCP):
565
  route.description or route.summary or f"Represents {route.path}"
566
  )
567
 
568
- # Format enhanced description
569
  enhanced_description = format_description_with_responses(
570
  base_description=base_description,
571
  responses=route.responses,
 
 
572
  )
573
 
574
  resource = OpenAPIResource(
@@ -600,16 +604,30 @@ class FastMCPOpenAPI(FastMCP):
600
  route.description or route.summary or f"Template for {route.path}"
601
  )
602
 
603
- # Format enhanced description
604
  enhanced_description = format_description_with_responses(
605
  base_description=base_description,
606
  responses=route.responses,
 
 
607
  )
608
 
609
  template_params_schema = {
610
  "type": "object",
611
  "properties": {
612
- p.name: p.schema_ for p in route.parameters if p.location == "path"
 
 
 
 
 
 
 
 
 
 
 
 
613
  },
614
  "required": [
615
  p.name for p in route.parameters if p.location == "path" and p.required
 
534
  or f"Executes {route.method} {route.path}"
535
  )
536
 
537
+ # Format enhanced description with parameters and request body
538
  enhanced_description = format_description_with_responses(
539
  base_description=base_description,
540
  responses=route.responses,
541
+ parameters=route.parameters,
542
+ request_body=route.request_body,
543
  )
544
 
545
  tool = OpenAPITool(
 
567
  route.description or route.summary or f"Represents {route.path}"
568
  )
569
 
570
+ # Format enhanced description with parameters and request body
571
  enhanced_description = format_description_with_responses(
572
  base_description=base_description,
573
  responses=route.responses,
574
+ parameters=route.parameters,
575
+ request_body=route.request_body,
576
  )
577
 
578
  resource = OpenAPIResource(
 
604
  route.description or route.summary or f"Template for {route.path}"
605
  )
606
 
607
+ # Format enhanced description with parameters and request body
608
  enhanced_description = format_description_with_responses(
609
  base_description=base_description,
610
  responses=route.responses,
611
+ parameters=route.parameters,
612
+ request_body=route.request_body,
613
  )
614
 
615
  template_params_schema = {
616
  "type": "object",
617
  "properties": {
618
+ p.name: {
619
+ **(p.schema_.copy() if isinstance(p.schema_, dict) else {}),
620
+ **(
621
+ {"description": p.description}
622
+ if p.description
623
+ and not (
624
+ isinstance(p.schema_, dict) and "description" in p.schema_
625
+ )
626
+ else {}
627
+ ),
628
+ }
629
+ for p in route.parameters
630
+ if p.location == "path"
631
  },
632
  "required": [
633
  p.name for p in route.parameters if p.location == "path" and p.required
src/fastmcp/utilities/openapi.py CHANGED
@@ -1001,53 +1001,84 @@ def format_description_with_responses(
1001
  responses: dict[
1002
  str, Any
1003
  ], # Changed from specific ResponseInfo type to avoid circular imports
 
 
1004
  ) -> str:
1005
- """Formats the base description string with response information."""
1006
- if not responses:
1007
- return base_description
1008
-
1009
  desc_parts = [base_description]
1010
- response_section = "\n\n**Responses:**"
1011
- added_response_section = False
1012
-
1013
- # Determine success codes (common ones)
1014
- success_codes = {"200", "201", "202", "204"} # As strings
1015
- success_status = next((s for s in success_codes if s in responses), None)
1016
-
1017
- # Process all responses
1018
- responses_to_process = responses.items()
1019
-
1020
- for status_code, resp_info in sorted(responses_to_process):
1021
- if not added_response_section:
1022
- desc_parts.append(response_section)
1023
- added_response_section = True
1024
-
1025
- status_marker = " (Success)" if status_code == success_status else ""
1026
- desc_parts.append(
1027
- f"\n- **{status_code}**{status_marker}: {resp_info.description or 'No description.'}"
1028
- )
1029
-
1030
- # Process content schemas for this response
1031
- if resp_info.content_schema:
1032
- # Prioritize json, then take first available
1033
- media_type = (
1034
- "application/json"
1035
- if "application/json" in resp_info.content_schema
1036
- else next(iter(resp_info.content_schema), None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1037
  )
1038
 
1039
- if media_type:
1040
- schema = resp_info.content_schema.get(media_type)
1041
- desc_parts.append(f" - Content-Type: `{media_type}`")
1042
-
1043
- if schema:
1044
- # Generate Example
1045
- example = generate_example_from_schema(schema)
1046
- if example != "unknown_type" and example is not None:
1047
- desc_parts.append("\n - **Example:**")
1048
- desc_parts.append(
1049
- format_json_for_description(example, indent=2)
1050
- )
 
 
 
 
 
 
 
 
 
1051
 
1052
  return "\n".join(desc_parts)
1053
 
@@ -1069,7 +1100,15 @@ def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
1069
  for param in route.parameters:
1070
  if param.required:
1071
  required.append(param.name)
1072
- properties[param.name] = param.schema_
 
 
 
 
 
 
 
 
1073
 
1074
  # Add request body if it exists
1075
  if route.request_body and route.request_body.content_schema:
@@ -1077,8 +1116,11 @@ def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
1077
  content_type = next(iter(route.request_body.content_schema))
1078
  body_schema = route.request_body.content_schema[content_type]
1079
  body_props = body_schema.get("properties", {})
 
 
1080
  for prop_name, prop_schema in body_props.items():
1081
  properties[prop_name] = prop_schema
 
1082
  if route.request_body.required:
1083
  required.extend(body_schema.get("required", []))
1084
 
 
1001
  responses: dict[
1002
  str, Any
1003
  ], # Changed from specific ResponseInfo type to avoid circular imports
1004
+ parameters: list[openapi.ParameterInfo] | None = None, # Add parameters parameter
1005
+ request_body: openapi.RequestBodyInfo | None = None, # Add request_body parameter
1006
  ) -> str:
1007
+ """Formats the base description string with response and parameter information."""
 
 
 
1008
  desc_parts = [base_description]
1009
+
1010
+ # Add parameter information
1011
+ if parameters:
1012
+ # Process path parameters
1013
+ path_params = [p for p in parameters if p.location == "path"]
1014
+ if path_params:
1015
+ param_section = "\n\n**Path Parameters:**"
1016
+ desc_parts.append(param_section)
1017
+ for param in path_params:
1018
+ required_marker = " (Required)" if param.required else ""
1019
+ param_desc = f"\n- **{param.name}**{required_marker}: {param.description or 'No description.'}"
1020
+ desc_parts.append(param_desc)
1021
+
1022
+ # Process query parameters
1023
+ query_params = [p for p in parameters if p.location == "query"]
1024
+ if query_params:
1025
+ param_section = "\n\n**Query Parameters:**"
1026
+ desc_parts.append(param_section)
1027
+ for param in query_params:
1028
+ required_marker = " (Required)" if param.required else ""
1029
+ param_desc = f"\n- **{param.name}**{required_marker}: {param.description or 'No description.'}"
1030
+ desc_parts.append(param_desc)
1031
+
1032
+ # Add request body information if present
1033
+ if request_body and request_body.description:
1034
+ req_body_section = "\n\n**Request Body:**"
1035
+ desc_parts.append(req_body_section)
1036
+ required_marker = " (Required)" if request_body.required else ""
1037
+ desc_parts.append(f"\n{request_body.description}{required_marker}")
1038
+
1039
+ # Add response information
1040
+ if responses:
1041
+ response_section = "\n\n**Responses:**"
1042
+ added_response_section = False
1043
+
1044
+ # Determine success codes (common ones)
1045
+ success_codes = {"200", "201", "202", "204"} # As strings
1046
+ success_status = next((s for s in success_codes if s in responses), None)
1047
+
1048
+ # Process all responses
1049
+ responses_to_process = responses.items()
1050
+
1051
+ for status_code, resp_info in sorted(responses_to_process):
1052
+ if not added_response_section:
1053
+ desc_parts.append(response_section)
1054
+ added_response_section = True
1055
+
1056
+ status_marker = " (Success)" if status_code == success_status else ""
1057
+ desc_parts.append(
1058
+ f"\n- **{status_code}**{status_marker}: {resp_info.description or 'No description.'}"
1059
  )
1060
 
1061
+ # Process content schemas for this response
1062
+ if resp_info.content_schema:
1063
+ # Prioritize json, then take first available
1064
+ media_type = (
1065
+ "application/json"
1066
+ if "application/json" in resp_info.content_schema
1067
+ else next(iter(resp_info.content_schema), None)
1068
+ )
1069
+
1070
+ if media_type:
1071
+ schema = resp_info.content_schema.get(media_type)
1072
+ desc_parts.append(f" - Content-Type: `{media_type}`")
1073
+
1074
+ if schema:
1075
+ # Generate Example
1076
+ example = generate_example_from_schema(schema)
1077
+ if example != "unknown_type" and example is not None:
1078
+ desc_parts.append("\n - **Example:**")
1079
+ desc_parts.append(
1080
+ format_json_for_description(example, indent=2)
1081
+ )
1082
 
1083
  return "\n".join(desc_parts)
1084
 
 
1100
  for param in route.parameters:
1101
  if param.required:
1102
  required.append(param.name)
1103
+
1104
+ # Copy the schema and add description if available
1105
+ param_schema = param.schema_.copy() if isinstance(param.schema_, dict) else {}
1106
+
1107
+ # Add parameter description to schema if available and not already present
1108
+ if param.description and not param_schema.get("description"):
1109
+ param_schema["description"] = param.description
1110
+
1111
+ properties[param.name] = param_schema
1112
 
1113
  # Add request body if it exists
1114
  if route.request_body and route.request_body.content_schema:
 
1116
  content_type = next(iter(route.request_body.content_schema))
1117
  body_schema = route.request_body.content_schema[content_type]
1118
  body_props = body_schema.get("properties", {})
1119
+
1120
+ # Add request body properties
1121
  for prop_name, prop_schema in body_props.items():
1122
  properties[prop_name] = prop_schema
1123
+
1124
  if route.request_body.required:
1125
  required.extend(body_schema.get("required", []))
1126
 
tests/server/test_openapi.py CHANGED
@@ -1034,3 +1034,319 @@ async def test_none_path_parameters_rejected(
1034
  "name": "New Name",
1035
  },
1036
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1034
  "name": "New Name",
1035
  },
1036
  )
1037
+
1038
+
1039
+ class TestDescriptionPropagation:
1040
+ """Tests for OpenAPI description propagation to FastMCP components.
1041
+
1042
+ Each test focuses on a single, specific behavior to make it immediately clear
1043
+ what's broken when a test fails.
1044
+ """
1045
+
1046
+ @pytest.fixture
1047
+ def simple_openapi_spec(self) -> dict:
1048
+ """Create a minimal OpenAPI spec with obvious test descriptions."""
1049
+ return {
1050
+ "openapi": "3.1.0",
1051
+ "info": {"title": "Test API", "version": "1.0.0"},
1052
+ "paths": {
1053
+ "/items": {
1054
+ "get": {
1055
+ "operationId": "listItems",
1056
+ "summary": "List items summary",
1057
+ "description": "LIST_DESCRIPTION",
1058
+ "responses": {
1059
+ "200": {"description": "LIST_RESPONSE_DESCRIPTION"}
1060
+ },
1061
+ }
1062
+ },
1063
+ "/items/{item_id}": {
1064
+ "get": {
1065
+ "operationId": "getItem",
1066
+ "summary": "Get item summary",
1067
+ "description": "GET_DESCRIPTION",
1068
+ "parameters": [
1069
+ {
1070
+ "name": "item_id",
1071
+ "in": "path",
1072
+ "required": True,
1073
+ "description": "PATH_PARAM_DESCRIPTION",
1074
+ "schema": {"type": "string"},
1075
+ },
1076
+ {
1077
+ "name": "fields",
1078
+ "in": "query",
1079
+ "required": False,
1080
+ "description": "QUERY_PARAM_DESCRIPTION",
1081
+ "schema": {"type": "string"},
1082
+ },
1083
+ ],
1084
+ "responses": {
1085
+ "200": {"description": "GET_RESPONSE_DESCRIPTION"}
1086
+ },
1087
+ }
1088
+ },
1089
+ "/items/create": {
1090
+ "post": {
1091
+ "operationId": "createItem",
1092
+ "summary": "Create item summary",
1093
+ "description": "CREATE_DESCRIPTION",
1094
+ "requestBody": {
1095
+ "required": True,
1096
+ "description": "BODY_DESCRIPTION",
1097
+ "content": {
1098
+ "application/json": {
1099
+ "schema": {
1100
+ "type": "object",
1101
+ "properties": {
1102
+ "name": {
1103
+ "type": "string",
1104
+ "description": "PROP_DESCRIPTION",
1105
+ }
1106
+ },
1107
+ "required": ["name"],
1108
+ }
1109
+ }
1110
+ },
1111
+ },
1112
+ "responses": {
1113
+ "201": {"description": "CREATE_RESPONSE_DESCRIPTION"}
1114
+ },
1115
+ }
1116
+ },
1117
+ },
1118
+ }
1119
+
1120
+ @pytest.fixture
1121
+ async def mock_client(self) -> httpx.AsyncClient:
1122
+ """Create a mock client that returns simple responses."""
1123
+
1124
+ async def _responder(request):
1125
+ if request.url.path == "/items" and request.method == "GET":
1126
+ return httpx.Response(200, json=[{"id": "1", "name": "Item 1"}])
1127
+ elif request.url.path.startswith("/items/") and request.method == "GET":
1128
+ item_id = request.url.path.split("/")[-1]
1129
+ return httpx.Response(
1130
+ 200, json={"id": item_id, "name": f"Item {item_id}"}
1131
+ )
1132
+ elif request.url.path == "/items/create" and request.method == "POST":
1133
+ import json
1134
+
1135
+ data = json.loads(request.content)
1136
+ return httpx.Response(201, json={"id": "new", "name": data.get("name")})
1137
+
1138
+ return httpx.Response(404)
1139
+
1140
+ transport = httpx.MockTransport(_responder)
1141
+ return httpx.AsyncClient(transport=transport, base_url="http://test")
1142
+
1143
+ @pytest.fixture
1144
+ async def test_server(self, simple_openapi_spec, mock_client):
1145
+ """Create a FastMCPOpenAPI server with the simple test spec."""
1146
+ return FastMCPOpenAPI(
1147
+ openapi_spec=simple_openapi_spec,
1148
+ client=mock_client,
1149
+ name="Test API",
1150
+ )
1151
+
1152
+ # --- RESOURCE TESTS ---
1153
+
1154
+ async def test_resource_includes_route_description(self, test_server):
1155
+ """Test that a Resource includes the route description."""
1156
+ resources = list(test_server._resource_manager.get_resources().values())
1157
+ list_resource = next((r for r in resources if r.name == "listItems"), None)
1158
+
1159
+ assert list_resource is not None, "listItems resource wasn't created"
1160
+ assert "LIST_DESCRIPTION" in (list_resource.description or ""), (
1161
+ "Route description missing from Resource"
1162
+ )
1163
+
1164
+ async def test_resource_includes_response_description(self, test_server):
1165
+ """Test that a Resource includes the response description."""
1166
+ resources = list(test_server._resource_manager.get_resources().values())
1167
+ list_resource = next((r for r in resources if r.name == "listItems"), None)
1168
+
1169
+ assert list_resource is not None, "listItems resource wasn't created"
1170
+ assert "LIST_RESPONSE_DESCRIPTION" in (list_resource.description or ""), (
1171
+ "Response description missing from Resource"
1172
+ )
1173
+
1174
+ # --- RESOURCE TEMPLATE TESTS ---
1175
+
1176
+ async def test_template_includes_route_description(self, test_server):
1177
+ """Test that a ResourceTemplate includes the route description."""
1178
+ templates = list(test_server._resource_manager.get_templates().values())
1179
+ get_template = next((t for t in templates if t.name == "getItem"), None)
1180
+
1181
+ assert get_template is not None, "getItem template wasn't created"
1182
+ assert "GET_DESCRIPTION" in (get_template.description or ""), (
1183
+ "Route description missing from ResourceTemplate"
1184
+ )
1185
+
1186
+ async def test_template_includes_path_parameter_description(self, test_server):
1187
+ """Test that a ResourceTemplate includes path parameter descriptions."""
1188
+ templates = list(test_server._resource_manager.get_templates().values())
1189
+ get_template = next((t for t in templates if t.name == "getItem"), None)
1190
+
1191
+ assert get_template is not None, "getItem template wasn't created"
1192
+ assert "PATH_PARAM_DESCRIPTION" in (get_template.description or ""), (
1193
+ "Path parameter description missing from ResourceTemplate description"
1194
+ )
1195
+
1196
+ async def test_template_includes_query_parameter_description(self, test_server):
1197
+ """Test that a ResourceTemplate includes query parameter descriptions."""
1198
+ templates = list(test_server._resource_manager.get_templates().values())
1199
+ get_template = next((t for t in templates if t.name == "getItem"), None)
1200
+
1201
+ assert get_template is not None, "getItem template wasn't created"
1202
+ assert "QUERY_PARAM_DESCRIPTION" in (get_template.description or ""), (
1203
+ "Query parameter description missing from ResourceTemplate description"
1204
+ )
1205
+
1206
+ async def test_template_includes_response_description(self, test_server):
1207
+ """Test that a ResourceTemplate includes response descriptions."""
1208
+ templates = list(test_server._resource_manager.get_templates().values())
1209
+ get_template = next((t for t in templates if t.name == "getItem"), None)
1210
+
1211
+ assert get_template is not None, "getItem template wasn't created"
1212
+ assert "GET_RESPONSE_DESCRIPTION" in (get_template.description or ""), (
1213
+ "Response description missing from ResourceTemplate description"
1214
+ )
1215
+
1216
+ async def test_template_parameter_schema_includes_description(self, test_server):
1217
+ """Test that a ResourceTemplate's parameter schema includes parameter descriptions."""
1218
+ templates = list(test_server._resource_manager.get_templates().values())
1219
+ get_template = next((t for t in templates if t.name == "getItem"), None)
1220
+
1221
+ assert get_template is not None, "getItem template wasn't created"
1222
+ assert "properties" in get_template.parameters, (
1223
+ "Schema properties missing from ResourceTemplate"
1224
+ )
1225
+ assert "item_id" in get_template.parameters["properties"], (
1226
+ "item_id missing from ResourceTemplate schema"
1227
+ )
1228
+ assert "description" in get_template.parameters["properties"]["item_id"], (
1229
+ "Description missing from item_id parameter schema"
1230
+ )
1231
+ assert (
1232
+ "PATH_PARAM_DESCRIPTION"
1233
+ in get_template.parameters["properties"]["item_id"]["description"]
1234
+ ), "Path parameter description incorrect in schema"
1235
+
1236
+ # --- TOOL TESTS ---
1237
+
1238
+ async def test_tool_includes_route_description(self, test_server):
1239
+ """Test that a Tool includes the route description."""
1240
+ tools = test_server._tool_manager.list_tools()
1241
+ create_tool = next((t for t in tools if t.name == "createItem"), None)
1242
+
1243
+ assert create_tool is not None, "createItem tool wasn't created"
1244
+ assert "CREATE_DESCRIPTION" in (create_tool.description or ""), (
1245
+ "Route description missing from Tool"
1246
+ )
1247
+
1248
+ async def test_tool_includes_request_body_description(self, test_server):
1249
+ """Test that a Tool includes the request body description."""
1250
+ tools = test_server._tool_manager.list_tools()
1251
+ create_tool = next((t for t in tools if t.name == "createItem"), None)
1252
+
1253
+ assert create_tool is not None, "createItem tool wasn't created"
1254
+ assert "BODY_DESCRIPTION" in (create_tool.description or ""), (
1255
+ "Request body description missing from Tool"
1256
+ )
1257
+
1258
+ async def test_tool_includes_response_description(self, test_server):
1259
+ """Test that a Tool includes response descriptions."""
1260
+ tools = test_server._tool_manager.list_tools()
1261
+ create_tool = next((t for t in tools if t.name == "createItem"), None)
1262
+
1263
+ assert create_tool is not None, "createItem tool wasn't created"
1264
+ assert "CREATE_RESPONSE_DESCRIPTION" in (create_tool.description or ""), (
1265
+ "Response description missing from Tool"
1266
+ )
1267
+
1268
+ async def test_tool_parameter_schema_includes_property_description(
1269
+ self, test_server
1270
+ ):
1271
+ """Test that a Tool's parameter schema includes property descriptions."""
1272
+ tools = test_server._tool_manager.list_tools()
1273
+ create_tool = next((t for t in tools if t.name == "createItem"), None)
1274
+
1275
+ assert create_tool is not None, "createItem tool wasn't created"
1276
+ assert "properties" in create_tool.parameters, (
1277
+ "Schema properties missing from Tool"
1278
+ )
1279
+ assert "name" in create_tool.parameters["properties"], (
1280
+ "name parameter missing from Tool schema"
1281
+ )
1282
+ assert "description" in create_tool.parameters["properties"]["name"], (
1283
+ "Description missing from name parameter schema"
1284
+ )
1285
+ assert (
1286
+ "PROP_DESCRIPTION"
1287
+ in create_tool.parameters["properties"]["name"]["description"]
1288
+ ), "Property description incorrect in schema"
1289
+
1290
+ # --- CLIENT API TESTS ---
1291
+
1292
+ async def test_client_api_resource_description(self, test_server):
1293
+ """Test that Resource descriptions are accessible via the client API."""
1294
+ async with Client(test_server) as client:
1295
+ resources = await client.list_resources()
1296
+ list_resource = next((r for r in resources if r.name == "listItems"), None)
1297
+
1298
+ assert list_resource is not None, (
1299
+ "listItems resource not accessible via client API"
1300
+ )
1301
+ assert "LIST_DESCRIPTION" in (list_resource.description or ""), (
1302
+ "Route description missing in Resource from client API"
1303
+ )
1304
+
1305
+ async def test_client_api_template_description(self, test_server):
1306
+ """Test that ResourceTemplate descriptions are accessible via the client API."""
1307
+ async with Client(test_server) as client:
1308
+ templates = await client.list_resource_templates()
1309
+ get_template = next((t for t in templates if t.name == "getItem"), None)
1310
+
1311
+ assert get_template is not None, (
1312
+ "getItem template not accessible via client API"
1313
+ )
1314
+ assert "GET_DESCRIPTION" in (get_template.description or ""), (
1315
+ "Route description missing in ResourceTemplate from client API"
1316
+ )
1317
+
1318
+ async def test_client_api_tool_description(self, test_server):
1319
+ """Test that Tool descriptions are accessible via the client API."""
1320
+ async with Client(test_server) as client:
1321
+ tools = await client.list_tools()
1322
+ create_tool = next((t for t in tools if t.name == "createItem"), None)
1323
+
1324
+ assert create_tool is not None, (
1325
+ "createItem tool not accessible via client API"
1326
+ )
1327
+ assert "CREATE_DESCRIPTION" in (create_tool.description or ""), (
1328
+ "Route description missing in Tool from client API"
1329
+ )
1330
+
1331
+ async def test_client_api_tool_parameter_schema(self, test_server):
1332
+ """Test that Tool parameter schemas are accessible via the client API."""
1333
+ async with Client(test_server) as client:
1334
+ tools = await client.list_tools()
1335
+ create_tool = next((t for t in tools if t.name == "createItem"), None)
1336
+
1337
+ assert create_tool is not None, (
1338
+ "createItem tool not accessible via client API"
1339
+ )
1340
+ assert "properties" in create_tool.inputSchema, (
1341
+ "Schema properties missing from Tool inputSchema in client API"
1342
+ )
1343
+ assert "name" in create_tool.inputSchema["properties"], (
1344
+ "name parameter missing from Tool schema in client API"
1345
+ )
1346
+ assert "description" in create_tool.inputSchema["properties"]["name"], (
1347
+ "Description missing from name parameter in client API"
1348
+ )
1349
+ assert (
1350
+ "PROP_DESCRIPTION"
1351
+ in create_tool.inputSchema["properties"]["name"]["description"]
1352
+ ), "Property description incorrect in schema from client API"