Jeremiah Lowin commited on
Commit
502b01e
·
unverified ·
2 Parent(s): a9a50bee1fcc29

Merge pull request #254 from jlowin/openapi-resource

Browse files
src/fastmcp/server/openapi.py CHANGED
@@ -257,7 +257,7 @@ class OpenAPIResource(Resource):
257
  self._client = client
258
  self._route = route
259
 
260
- async def read(self) -> str:
261
  """Fetch the resource data by making an HTTP request."""
262
  try:
263
  # Extract path parameters from the URI if present
@@ -297,15 +297,16 @@ class OpenAPIResource(Resource):
297
  # Raise for 4xx/5xx responses
298
  response.raise_for_status()
299
 
300
- # Return response content based on mime type
301
- if self.mime_type == "application/json":
302
- try:
303
- return response.json()
304
- except (json.JSONDecodeError, ValueError):
305
- # Fallback to returning the text
306
- return response.text
307
- else:
308
  return response.text
 
 
309
 
310
  except httpx.HTTPStatusError as e:
311
  # Handle HTTP errors (4xx, 5xx)
@@ -343,59 +344,13 @@ class OpenAPIResourceTemplate(ResourceTemplate):
343
  uri_template=uri_template,
344
  name=name,
345
  description=description,
346
- fn=self._create_resource_fn,
347
  parameters=parameters,
348
  tags=tags,
349
  )
350
  self._client = client
351
  self._route = route
352
 
353
- async def _create_resource_fn(self, **kwargs):
354
- """Create a resource with parameters."""
355
- # Prepare the path with parameters
356
- path = self._route.path
357
- for param_name, param_value in kwargs.items():
358
- path = path.replace(f"{{{param_name}}}", str(param_value))
359
-
360
- try:
361
- response = await self._client.request(
362
- method=self._route.method,
363
- url=path,
364
- timeout=30.0, # Default timeout
365
- )
366
-
367
- # Raise for 4xx/5xx responses
368
- response.raise_for_status()
369
-
370
- # Determine the mime type from the response
371
- content_type = response.headers.get("content-type", "application/json")
372
- mime_type = content_type.split(";")[0].strip()
373
-
374
- # Return the appropriate data
375
- if mime_type == "application/json":
376
- try:
377
- return response.json()
378
- except (json.JSONDecodeError, ValueError):
379
- return response.text
380
- else:
381
- return response.text
382
-
383
- except httpx.HTTPStatusError as e:
384
- error_message = (
385
- f"HTTP error {e.response.status_code}: {e.response.reason_phrase}"
386
- )
387
- try:
388
- error_data = e.response.json()
389
- error_message += f" - {error_data}"
390
- except (json.JSONDecodeError, ValueError):
391
- if e.response.text:
392
- error_message += f" - {e.response.text}"
393
-
394
- raise ValueError(error_message)
395
-
396
- except httpx.RequestError as e:
397
- raise ValueError(f"Request error: {str(e)}")
398
-
399
  async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
400
  """Create a resource with the given parameters."""
401
  # Generate a URI for this resource instance
@@ -409,9 +364,8 @@ class OpenAPIResourceTemplate(ResourceTemplate):
409
  route=self._route,
410
  uri=uri,
411
  name=f"{self.name}-{'-'.join(uri_parts)}",
412
- description=self.description
413
- or f"Resource for {self._route.path}", # Provide default if None
414
- mime_type="application/json", # Default, will be updated when read
415
  tags=set(self._route.tags or []),
416
  )
417
 
 
257
  self._client = client
258
  self._route = route
259
 
260
+ async def read(self) -> str | bytes:
261
  """Fetch the resource data by making an HTTP request."""
262
  try:
263
  # Extract path parameters from the URI if present
 
297
  # Raise for 4xx/5xx responses
298
  response.raise_for_status()
299
 
300
+ # Determine content type and return appropriate format
301
+ content_type = response.headers.get("content-type", "").lower()
302
+
303
+ if "application/json" in content_type:
304
+ result = response.json()
305
+ return json.dumps(result)
306
+ elif any(ct in content_type for ct in ["text/", "application/xml"]):
 
307
  return response.text
308
+ else:
309
+ return response.content
310
 
311
  except httpx.HTTPStatusError as e:
312
  # Handle HTTP errors (4xx, 5xx)
 
344
  uri_template=uri_template,
345
  name=name,
346
  description=description,
347
+ fn=lambda **kwargs: None,
348
  parameters=parameters,
349
  tags=tags,
350
  )
351
  self._client = client
352
  self._route = route
353
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
355
  """Create a resource with the given parameters."""
356
  # Generate a URI for this resource instance
 
364
  route=self._route,
365
  uri=uri,
366
  name=f"{self.name}-{'-'.join(uri_parts)}",
367
+ description=self.description or f"Resource for {self._route.path}",
368
+ mime_type="application/json",
 
369
  tags=set(self._route.tags or []),
370
  )
371
 
tests/server/test_openapi.py CHANGED
@@ -1,12 +1,14 @@
 
1
  import json
2
  import re
3
 
4
  import httpx
5
  import pytest
6
  from dirty_equals import IsStr
7
- from fastapi import FastAPI, HTTPException
 
8
  from httpx import ASGITransport, AsyncClient
9
- from mcp.types import TextContent
10
  from pydantic import BaseModel, TypeAdapter
11
  from pydantic.networks import AnyUrl
12
 
@@ -66,6 +68,17 @@ def fastapi_app(users_db: dict[int, User]) -> FastAPI:
66
  user.name = name
67
  return user
68
 
 
 
 
 
 
 
 
 
 
 
 
69
  return app
70
 
71
 
@@ -120,7 +133,8 @@ class TestTools:
120
  """
121
  By default, tools exclude GET methods
122
  """
123
- tools = await fastmcp_openapi_server._mcp_list_tools()
 
124
  assert len(tools) == 2
125
 
126
  assert tools[0].model_dump() == dict(
@@ -156,9 +170,10 @@ class TestTools:
156
  """
157
  The tool created by the OpenAPI server should be the same as the original
158
  """
159
- tool_response = await fastmcp_openapi_server._mcp_call_tool(
160
- "create_user_users_post", {"name": "David", "active": False}
161
- )
 
162
 
163
  # Convert TextContent to dict for comparison
164
  assert isinstance(tool_response, list) and len(tool_response) == 1
@@ -173,10 +188,13 @@ class TestTools:
173
  assert len(response.json()) == 4
174
 
175
  # Check that the user was created via MCP
176
- user_response = await fastmcp_openapi_server._mcp_read_resource(
177
- "resource://openapi/get_user_users__user_id__get/4"
178
- )
179
- user = user_response[0].content
 
 
 
180
  assert user == expected_user
181
 
182
  async def test_call_update_user_name_tool(
@@ -185,9 +203,11 @@ class TestTools:
185
  """
186
  The tool created by the OpenAPI server should be the same as the original
187
  """
188
- tool_response = await fastmcp_openapi_server._mcp_call_tool(
189
- "update_user_name_users__user_id__name_patch", {"user_id": 1, "name": "XYZ"}
190
- )
 
 
191
 
192
  # Convert TextContent to dict for comparison
193
  assert isinstance(tool_response, list) and len(tool_response) == 1
@@ -202,10 +222,13 @@ class TestTools:
202
  assert expected_data in response.json()
203
 
204
  # Check that the user was updated via MCP
205
- user_response = await fastmcp_openapi_server._mcp_read_resource(
206
- "resource://openapi/get_user_users__user_id__get/1"
207
- )
208
- user = user_response[0].content
 
 
 
209
  assert user == expected_data
210
 
211
 
@@ -214,8 +237,9 @@ class TestResources:
214
  """
215
  By default, resources exclude GET methods without parameters
216
  """
217
- resources = await fastmcp_openapi_server._mcp_list_resources()
218
- assert len(resources) == 1
 
219
  assert resources[0].uri == AnyUrl("resource://openapi/get_users_users_get")
220
  assert resources[0].name == "get_users_users_get"
221
 
@@ -228,17 +252,47 @@ class TestResources:
228
  """
229
  The resource created by the OpenAPI server should be the same as the original
230
  """
 
231
  json_users = TypeAdapter(list[User]).dump_python(
232
  sorted(users_db.values(), key=lambda x: x.id)
233
  )
234
- resource_response = await fastmcp_openapi_server._mcp_read_resource(
235
- "resource://openapi/get_users_users_get"
236
- )
237
- resource = resource_response[0].content
 
 
 
238
  assert resource == json_users
239
  response = await api_client.get("/users")
240
  assert response.json() == json_users
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  class TestResourceTemplates:
244
  async def test_list_resource_templates(
@@ -247,7 +301,8 @@ class TestResourceTemplates:
247
  """
248
  By default, resource templates exclude GET methods without parameters
249
  """
250
- resource_templates = await fastmcp_openapi_server._mcp_list_resource_templates()
 
251
  assert len(resource_templates) == 1
252
  assert resource_templates[0].name == "get_user_users__user_id__get"
253
  assert (
@@ -265,11 +320,14 @@ class TestResourceTemplates:
265
  The resource template created by the OpenAPI server should be the same as the original
266
  """
267
  user_id = 2
268
- resource_response = await fastmcp_openapi_server._mcp_read_resource(
269
- f"resource://openapi/get_user_users__user_id__get/{user_id}"
270
- )
 
 
 
 
271
 
272
- resource = resource_response[0].content
273
  assert resource == users_db[user_id].model_dump()
274
  response = await api_client.get(f"/users/{user_id}")
275
  assert resource == response.json()
@@ -280,7 +338,8 @@ class TestPrompts:
280
  """
281
  By default, there are no prompts.
282
  """
283
- prompts = await fastmcp_openapi_server._mcp_list_prompts()
 
284
  assert len(prompts) == 0
285
 
286
 
@@ -494,20 +553,23 @@ class TestOpenAPI30Compatibility:
494
 
495
  async def test_resource_discovery(self, openapi_30_server):
496
  """Test that resources are correctly discovered from an OpenAPI 3.0 spec."""
497
- resources = await openapi_30_server._mcp_list_resources()
 
498
  assert len(resources) == 1
499
  assert resources[0].uri == AnyUrl("resource://openapi/listProducts")
500
 
501
  async def test_resource_template_discovery(self, openapi_30_server):
502
  """Test that resource templates are correctly discovered from an OpenAPI 3.0 spec."""
503
- templates = await openapi_30_server._mcp_list_resource_templates()
 
504
  assert len(templates) == 1
505
  assert templates[0].name == "getProduct"
506
  assert templates[0].uriTemplate == r"resource://openapi/getProduct/{product_id}"
507
 
508
  async def test_tool_discovery(self, openapi_30_server):
509
  """Test that tools are correctly discovered from an OpenAPI 3.0 spec."""
510
- tools = await openapi_30_server._mcp_list_tools()
 
511
  assert len(tools) == 1
512
  assert tools[0].name == "createProduct"
513
  assert "name" in tools[0].inputSchema["properties"]
@@ -515,20 +577,26 @@ class TestOpenAPI30Compatibility:
515
 
516
  async def test_resource_access(self, openapi_30_server):
517
  """Test reading a resource from an OpenAPI 3.0 server."""
518
- resource_response = await openapi_30_server._mcp_read_resource(
519
- "resource://openapi/listProducts"
520
- )
521
- content = resource_response[0].content
 
 
 
522
  assert len(content) == 2
523
  assert content[0]["name"] == "Product 1"
524
  assert content[1]["name"] == "Product 2"
525
 
526
  async def test_resource_template_access(self, openapi_30_server):
527
  """Test reading a resource from template from an OpenAPI 3.0 server."""
528
- resource_response = await openapi_30_server._mcp_read_resource(
529
- "resource://openapi/getProduct/p1"
530
- )
531
- content = resource_response[0].content
 
 
 
532
  assert content["id"] == "p1"
533
  assert content["name"] == "Product 1"
534
  assert content["price"] == 19.99
@@ -665,20 +733,23 @@ class TestOpenAPI31Compatibility:
665
 
666
  async def test_resource_discovery(self, openapi_31_server):
667
  """Test that resources are correctly discovered from an OpenAPI 3.1 spec."""
668
- resources = await openapi_31_server._mcp_list_resources()
 
669
  assert len(resources) == 1
670
  assert resources[0].uri == AnyUrl("resource://openapi/listOrders")
671
 
672
  async def test_resource_template_discovery(self, openapi_31_server):
673
  """Test that resource templates are correctly discovered from an OpenAPI 3.1 spec."""
674
- templates = await openapi_31_server._mcp_list_resource_templates()
 
675
  assert len(templates) == 1
676
  assert templates[0].name == "getOrder"
677
  assert templates[0].uriTemplate == r"resource://openapi/getOrder/{order_id}"
678
 
679
  async def test_tool_discovery(self, openapi_31_server):
680
  """Test that tools are correctly discovered from an OpenAPI 3.1 spec."""
681
- tools = await openapi_31_server._mcp_list_tools()
 
682
  assert len(tools) == 1
683
  assert tools[0].name == "createOrder"
684
  assert "customer" in tools[0].inputSchema["properties"]
@@ -686,20 +757,26 @@ class TestOpenAPI31Compatibility:
686
 
687
  async def test_resource_access(self, openapi_31_server):
688
  """Test reading a resource from an OpenAPI 3.1 server."""
689
- resource_response = await openapi_31_server._mcp_read_resource(
690
- "resource://openapi/listOrders"
691
- )
692
- content = resource_response[0].content
 
 
 
693
  assert len(content) == 2
694
  assert content[0]["customer"] == "Alice"
695
  assert content[1]["customer"] == "Bob"
696
 
697
  async def test_resource_template_access(self, openapi_31_server):
698
  """Test reading a resource from template from an OpenAPI 3.1 server."""
699
- resource_response = await openapi_31_server._mcp_read_resource(
700
- "resource://openapi/getOrder/o1"
701
- )
702
- content = resource_response[0].content
 
 
 
703
  assert content["id"] == "o1"
704
  assert content["customer"] == "Alice"
705
  assert content["items"] == ["item1", "item2"]
@@ -729,8 +806,9 @@ class TestMountFastMCP:
729
  await mcp.import_server("fastapi", fastmcp_openapi_server)
730
 
731
  # Check that resources are available with prefixed URIs
732
- resources = await mcp._mcp_list_resources()
733
- assert len(resources) == 1
 
734
  # We're checking the key used by mcp to store the resource
735
  # The prefixed URI is used as the key, but the resource's original uri is preserved
736
  prefixed_uri = "fastapi+resource://openapi/get_users_users_get"
@@ -738,7 +816,8 @@ class TestMountFastMCP:
738
  assert resource is not None
739
 
740
  # Check that templates are available with prefixed URIs
741
- templates = await mcp._mcp_list_resource_templates()
 
742
  assert len(templates) == 1
743
  assert templates[0].name == "get_user_users__user_id__get"
744
  prefixed_template_uri = (
@@ -748,10 +827,12 @@ class TestMountFastMCP:
748
  assert template is not None
749
 
750
  # Check that tools are available with prefixed names
751
- tools = await mcp._mcp_list_tools()
 
752
  assert len(tools) == 2
753
  assert tools[0].name == "fastapi_create_user_users_post"
754
  assert tools[1].name == "fastapi_update_user_name_users__user_id__name_patch"
755
 
756
- prompts = await mcp._mcp_list_prompts()
 
757
  assert len(prompts) == 0
 
1
+ import base64
2
  import json
3
  import re
4
 
5
  import httpx
6
  import pytest
7
  from dirty_equals import IsStr
8
+ from fastapi import FastAPI, HTTPException, Response
9
+ from fastapi.responses import PlainTextResponse
10
  from httpx import ASGITransport, AsyncClient
11
+ from mcp.types import BlobResourceContents, TextContent, TextResourceContents
12
  from pydantic import BaseModel, TypeAdapter
13
  from pydantic.networks import AnyUrl
14
 
 
68
  user.name = name
69
  return user
70
 
71
+ @app.get("/ping", response_class=PlainTextResponse)
72
+ async def ping() -> str:
73
+ """Ping the server."""
74
+ return "pong"
75
+
76
+ @app.get("/ping-bytes")
77
+ async def ping_bytes() -> Response:
78
+ """Ping the server and get a bytes response."""
79
+
80
+ return Response(content=b"pong")
81
+
82
  return app
83
 
84
 
 
133
  """
134
  By default, tools exclude GET methods
135
  """
136
+ async with Client(fastmcp_openapi_server) as client:
137
+ tools = await client.list_tools()
138
  assert len(tools) == 2
139
 
140
  assert tools[0].model_dump() == dict(
 
170
  """
171
  The tool created by the OpenAPI server should be the same as the original
172
  """
173
+ async with Client(fastmcp_openapi_server) as client:
174
+ tool_response = await client.call_tool(
175
+ "create_user_users_post", {"name": "David", "active": False}
176
+ )
177
 
178
  # Convert TextContent to dict for comparison
179
  assert isinstance(tool_response, list) and len(tool_response) == 1
 
188
  assert len(response.json()) == 4
189
 
190
  # Check that the user was created via MCP
191
+ async with Client(fastmcp_openapi_server) as client:
192
+ user_response = await client.read_resource(
193
+ "resource://openapi/get_user_users__user_id__get/4"
194
+ )
195
+ assert isinstance(user_response[0], TextResourceContents)
196
+ response_text = user_response[0].text
197
+ user = json.loads(response_text)
198
  assert user == expected_user
199
 
200
  async def test_call_update_user_name_tool(
 
203
  """
204
  The tool created by the OpenAPI server should be the same as the original
205
  """
206
+ async with Client(fastmcp_openapi_server) as client:
207
+ tool_response = await client.call_tool(
208
+ "update_user_name_users__user_id__name_patch",
209
+ {"user_id": 1, "name": "XYZ"},
210
+ )
211
 
212
  # Convert TextContent to dict for comparison
213
  assert isinstance(tool_response, list) and len(tool_response) == 1
 
222
  assert expected_data in response.json()
223
 
224
  # Check that the user was updated via MCP
225
+ async with Client(fastmcp_openapi_server) as client:
226
+ user_response = await client.read_resource(
227
+ "resource://openapi/get_user_users__user_id__get/1"
228
+ )
229
+ assert isinstance(user_response[0], TextResourceContents)
230
+ response_text = user_response[0].text
231
+ user = json.loads(response_text)
232
  assert user == expected_data
233
 
234
 
 
237
  """
238
  By default, resources exclude GET methods without parameters
239
  """
240
+ async with Client(fastmcp_openapi_server) as client:
241
+ resources = await client.list_resources()
242
+ assert len(resources) == 3
243
  assert resources[0].uri == AnyUrl("resource://openapi/get_users_users_get")
244
  assert resources[0].name == "get_users_users_get"
245
 
 
252
  """
253
  The resource created by the OpenAPI server should be the same as the original
254
  """
255
+
256
  json_users = TypeAdapter(list[User]).dump_python(
257
  sorted(users_db.values(), key=lambda x: x.id)
258
  )
259
+ async with Client(fastmcp_openapi_server) as client:
260
+ resource_response = await client.read_resource(
261
+ "resource://openapi/get_users_users_get"
262
+ )
263
+ assert isinstance(resource_response[0], TextResourceContents)
264
+ response_text = resource_response[0].text
265
+ resource = json.loads(response_text)
266
  assert resource == json_users
267
  response = await api_client.get("/users")
268
  assert response.json() == json_users
269
 
270
+ async def test_get_bytes_resource(
271
+ self,
272
+ fastmcp_openapi_server: FastMCPOpenAPI,
273
+ api_client,
274
+ ):
275
+ """Test reading a resource that returns bytes."""
276
+ async with Client(fastmcp_openapi_server) as client:
277
+ resource_response = await client.read_resource(
278
+ "resource://openapi/ping_bytes_ping_bytes_get"
279
+ )
280
+ assert isinstance(resource_response[0], BlobResourceContents)
281
+ assert base64.b64decode(resource_response[0].blob) == b"pong"
282
+
283
+ async def test_get_str_resource(
284
+ self,
285
+ fastmcp_openapi_server: FastMCPOpenAPI,
286
+ api_client,
287
+ ):
288
+ """Test reading a resource that returns a string."""
289
+ async with Client(fastmcp_openapi_server) as client:
290
+ resource_response = await client.read_resource(
291
+ "resource://openapi/ping_ping_get"
292
+ )
293
+ assert isinstance(resource_response[0], TextResourceContents)
294
+ assert resource_response[0].text == "pong"
295
+
296
 
297
  class TestResourceTemplates:
298
  async def test_list_resource_templates(
 
301
  """
302
  By default, resource templates exclude GET methods without parameters
303
  """
304
+ async with Client(fastmcp_openapi_server) as client:
305
+ resource_templates = await client.list_resource_templates()
306
  assert len(resource_templates) == 1
307
  assert resource_templates[0].name == "get_user_users__user_id__get"
308
  assert (
 
320
  The resource template created by the OpenAPI server should be the same as the original
321
  """
322
  user_id = 2
323
+ async with Client(fastmcp_openapi_server) as client:
324
+ resource_response = await client.read_resource(
325
+ f"resource://openapi/get_user_users__user_id__get/{user_id}"
326
+ )
327
+ assert isinstance(resource_response[0], TextResourceContents)
328
+ response_text = resource_response[0].text
329
+ resource = json.loads(response_text)
330
 
 
331
  assert resource == users_db[user_id].model_dump()
332
  response = await api_client.get(f"/users/{user_id}")
333
  assert resource == response.json()
 
338
  """
339
  By default, there are no prompts.
340
  """
341
+ async with Client(fastmcp_openapi_server) as client:
342
+ prompts = await client.list_prompts()
343
  assert len(prompts) == 0
344
 
345
 
 
553
 
554
  async def test_resource_discovery(self, openapi_30_server):
555
  """Test that resources are correctly discovered from an OpenAPI 3.0 spec."""
556
+ async with Client(openapi_30_server) as client:
557
+ resources = await client.list_resources()
558
  assert len(resources) == 1
559
  assert resources[0].uri == AnyUrl("resource://openapi/listProducts")
560
 
561
  async def test_resource_template_discovery(self, openapi_30_server):
562
  """Test that resource templates are correctly discovered from an OpenAPI 3.0 spec."""
563
+ async with Client(openapi_30_server) as client:
564
+ templates = await client.list_resource_templates()
565
  assert len(templates) == 1
566
  assert templates[0].name == "getProduct"
567
  assert templates[0].uriTemplate == r"resource://openapi/getProduct/{product_id}"
568
 
569
  async def test_tool_discovery(self, openapi_30_server):
570
  """Test that tools are correctly discovered from an OpenAPI 3.0 spec."""
571
+ async with Client(openapi_30_server) as client:
572
+ tools = await client.list_tools()
573
  assert len(tools) == 1
574
  assert tools[0].name == "createProduct"
575
  assert "name" in tools[0].inputSchema["properties"]
 
577
 
578
  async def test_resource_access(self, openapi_30_server):
579
  """Test reading a resource from an OpenAPI 3.0 server."""
580
+ async with Client(openapi_30_server) as client:
581
+ resource_response = await client.read_resource(
582
+ "resource://openapi/listProducts"
583
+ )
584
+ assert isinstance(resource_response[0], TextResourceContents)
585
+ response_text = resource_response[0].text
586
+ content = json.loads(response_text)
587
  assert len(content) == 2
588
  assert content[0]["name"] == "Product 1"
589
  assert content[1]["name"] == "Product 2"
590
 
591
  async def test_resource_template_access(self, openapi_30_server):
592
  """Test reading a resource from template from an OpenAPI 3.0 server."""
593
+ async with Client(openapi_30_server) as client:
594
+ resource_response = await client.read_resource(
595
+ "resource://openapi/getProduct/p1"
596
+ )
597
+ assert isinstance(resource_response[0], TextResourceContents)
598
+ response_text = resource_response[0].text
599
+ content = json.loads(response_text)
600
  assert content["id"] == "p1"
601
  assert content["name"] == "Product 1"
602
  assert content["price"] == 19.99
 
733
 
734
  async def test_resource_discovery(self, openapi_31_server):
735
  """Test that resources are correctly discovered from an OpenAPI 3.1 spec."""
736
+ async with Client(openapi_31_server) as client:
737
+ resources = await client.list_resources()
738
  assert len(resources) == 1
739
  assert resources[0].uri == AnyUrl("resource://openapi/listOrders")
740
 
741
  async def test_resource_template_discovery(self, openapi_31_server):
742
  """Test that resource templates are correctly discovered from an OpenAPI 3.1 spec."""
743
+ async with Client(openapi_31_server) as client:
744
+ templates = await client.list_resource_templates()
745
  assert len(templates) == 1
746
  assert templates[0].name == "getOrder"
747
  assert templates[0].uriTemplate == r"resource://openapi/getOrder/{order_id}"
748
 
749
  async def test_tool_discovery(self, openapi_31_server):
750
  """Test that tools are correctly discovered from an OpenAPI 3.1 spec."""
751
+ async with Client(openapi_31_server) as client:
752
+ tools = await client.list_tools()
753
  assert len(tools) == 1
754
  assert tools[0].name == "createOrder"
755
  assert "customer" in tools[0].inputSchema["properties"]
 
757
 
758
  async def test_resource_access(self, openapi_31_server):
759
  """Test reading a resource from an OpenAPI 3.1 server."""
760
+ async with Client(openapi_31_server) as client:
761
+ resource_response = await client.read_resource(
762
+ "resource://openapi/listOrders"
763
+ )
764
+ assert isinstance(resource_response[0], TextResourceContents)
765
+ response_text = resource_response[0].text
766
+ content = json.loads(response_text)
767
  assert len(content) == 2
768
  assert content[0]["customer"] == "Alice"
769
  assert content[1]["customer"] == "Bob"
770
 
771
  async def test_resource_template_access(self, openapi_31_server):
772
  """Test reading a resource from template from an OpenAPI 3.1 server."""
773
+ async with Client(openapi_31_server) as client:
774
+ resource_response = await client.read_resource(
775
+ "resource://openapi/getOrder/o1"
776
+ )
777
+ assert isinstance(resource_response[0], TextResourceContents)
778
+ response_text = resource_response[0].text
779
+ content = json.loads(response_text)
780
  assert content["id"] == "o1"
781
  assert content["customer"] == "Alice"
782
  assert content["items"] == ["item1", "item2"]
 
806
  await mcp.import_server("fastapi", fastmcp_openapi_server)
807
 
808
  # Check that resources are available with prefixed URIs
809
+ async with Client(mcp) as client:
810
+ resources = await client.list_resources()
811
+ assert len(resources) == 3
812
  # We're checking the key used by mcp to store the resource
813
  # The prefixed URI is used as the key, but the resource's original uri is preserved
814
  prefixed_uri = "fastapi+resource://openapi/get_users_users_get"
 
816
  assert resource is not None
817
 
818
  # Check that templates are available with prefixed URIs
819
+ async with Client(mcp) as client:
820
+ templates = await client.list_resource_templates()
821
  assert len(templates) == 1
822
  assert templates[0].name == "get_user_users__user_id__get"
823
  prefixed_template_uri = (
 
827
  assert template is not None
828
 
829
  # Check that tools are available with prefixed names
830
+ async with Client(mcp) as client:
831
+ tools = await client.list_tools()
832
  assert len(tools) == 2
833
  assert tools[0].name == "fastapi_create_user_users_post"
834
  assert tools[1].name == "fastapi_update_user_name_users__user_id__name_patch"
835
 
836
+ async with Client(mcp) as client:
837
+ prompts = await client.list_prompts()
838
  assert len(prompts) == 0