mcp-openapi-proxy 0.1.1741477332__tar.gz → 0.1.1741479690__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (15) hide show
  1. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/PKG-INFO +5 -5
  2. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/README.md +3 -4
  3. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy/server_fastmcp.py +9 -14
  4. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy/server_lowlevel.py +14 -14
  5. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy/utils.py +23 -75
  6. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy.egg-info/PKG-INFO +5 -5
  7. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/pyproject.toml +3 -1
  8. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/LICENSE +0 -0
  9. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy/__init__.py +0 -0
  10. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy.egg-info/SOURCES.txt +0 -0
  11. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy.egg-info/dependency_links.txt +0 -0
  12. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy.egg-info/entry_points.txt +0 -0
  13. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy.egg-info/requires.txt +0 -0
  14. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/mcp_openapi_proxy.egg-info/top_level.txt +0 -0
  15. {mcp_openapi_proxy-0.1.1741477332 → mcp_openapi_proxy-0.1.1741479690}/setup.cfg +0 -0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mcp-openapi-proxy
3
- Version: 0.1.1741477332
3
+ Version: 0.1.1741479690
4
4
  Summary: MCP server for exposing OpenAPI specifications as MCP tools.
5
5
  Author-email: Matthew Hand <matthewhandau@gmail.com>
6
+ Requires-Python: >=3.10
6
7
  Description-Content-Type: text/markdown
7
8
  License-File: LICENSE
8
9
  Requires-Dist: mcp[cli]>=1.2.0
@@ -197,9 +198,8 @@ Update your configuration:
197
198
  "env": {
198
199
  "OPENAPI_SPEC_URL": "https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json",
199
200
  "TOOL_WHITELIST": "/chat,/bots,/conversations,/reminders,/files,/users",
200
- "API_KEY": "<your_slack_bot_token>",
201
- "API_KEY_JMESPATH": "token",
202
- "TOOL_NAME_PREFIX": "slack_"
201
+ "API_KEY": "<your_slack_bot_token, starts with xoxb>",
202
+ "STRIP_PARAM": "token"
203
203
  }
204
204
  }
205
205
  }
@@ -234,7 +234,7 @@ Try these commands in your MCP client:
234
234
 
235
235
  ### GetZep Example
236
236
 
237
- ![image](https://github.com/user-attachments/assets/6ae7f708-9494-41a1-9075-e685f2cd8873)
237
+ ![image](https://github.com/user-attachments/assets/9a4fdabb-fa3d-4626-a50f-438147eadc9f)
238
238
 
239
239
  GetZep offers a free cloud API for memory management with detailed endpoints. Since GetZep did not provide an official OpenAPI specification, this project includes a generated spec hosted on GitHub for convenience. This approach—creating a spec from documentation—is a reusable pattern: users can similarly generate OpenAPI specs for any REST API and reference them locally (e.g., `file:///path/to/spec.json`). Obtain an API key from [GetZep's documentation](https://docs.getzep.com/).
240
240
 
@@ -181,9 +181,8 @@ Update your configuration:
181
181
  "env": {
182
182
  "OPENAPI_SPEC_URL": "https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json",
183
183
  "TOOL_WHITELIST": "/chat,/bots,/conversations,/reminders,/files,/users",
184
- "API_KEY": "<your_slack_bot_token>",
185
- "API_KEY_JMESPATH": "token",
186
- "TOOL_NAME_PREFIX": "slack_"
184
+ "API_KEY": "<your_slack_bot_token, starts with xoxb>",
185
+ "STRIP_PARAM": "token"
187
186
  }
188
187
  }
189
188
  }
@@ -218,7 +217,7 @@ Try these commands in your MCP client:
218
217
 
219
218
  ### GetZep Example
220
219
 
221
- ![image](https://github.com/user-attachments/assets/6ae7f708-9494-41a1-9075-e685f2cd8873)
220
+ ![image](https://github.com/user-attachments/assets/9a4fdabb-fa3d-4626-a50f-438147eadc9f)
222
221
 
223
222
  GetZep offers a free cloud API for memory management with detailed endpoints. Since GetZep did not provide an official OpenAPI specification, this project includes a generated spec hosted on GitHub for convenience. This approach—creating a spec from documentation—is a reusable pattern: users can similarly generate OpenAPI specs for any REST API and reference them locally (e.g., `file:///path/to/spec.json`). Obtain an API key from [GetZep's documentation](https://docs.getzep.com/).
224
223
 
@@ -7,9 +7,8 @@ Configuration is controlled via environment variables:
7
7
  - OPENAPI_SPEC_URL_<hash>: Unique URL per test, falls back to OPENAPI_SPEC_URL.
8
8
  - TOOL_WHITELIST: Comma-separated list of allowed endpoint paths.
9
9
  - SERVER_URL_OVERRIDE: Optional override for the base URL from the OpenAPI spec.
10
- - API_KEY: Optional token for endpoints requiring authentication.
11
- - API_AUTH_TYPE: Optional type like 'Bearer' or 'Api-Key'.
12
- - API_KEY_JMESPATH: Optional JMESPath expression to map API_KEY into request parameters.
10
+ - API_KEY: Generic token for Bearer header.
11
+ - STRIP_PARAM: Param name (e.g., "auth") to remove from parameters.
13
12
  """
14
13
 
15
14
  import os
@@ -19,7 +18,7 @@ import requests
19
18
  from typing import Dict, Any
20
19
  from mcp import types
21
20
  from mcp.server.fastmcp import FastMCP
22
- from mcp_openapi_proxy.utils import setup_logging, is_tool_whitelisted, fetch_openapi_spec, get_auth_headers, build_base_url, normalize_tool_name, handle_custom_auth, get_tool_prefix
21
+ from mcp_openapi_proxy.utils import setup_logging, is_tool_whitelisted, fetch_openapi_spec, build_base_url, normalize_tool_name, handle_auth, strip_parameters, get_tool_prefix
23
22
 
24
23
  logger = setup_logging(debug=os.getenv("DEBUG", "").lower() in ("true", "1", "yes"))
25
24
 
@@ -112,8 +111,8 @@ def list_functions(*, env_key: str = "OPENAPI_SPEC_URL") -> str:
112
111
  def call_function(*, function_name: str, parameters: dict = None, env_key: str = "OPENAPI_SPEC_URL") -> str:
113
112
  """Calls a function derived from the OpenAPI specification."""
114
113
  logger.debug(f"call_function invoked with function_name='{function_name}' and parameters={parameters}")
115
- logger.debug(f"API_KEY from env: {os.getenv('API_KEY', '<not set>')[:5] + '...' if os.getenv('API_KEY') else '<not set>'}")
116
- logger.debug(f"API_KEY_JMESPATH from env: {os.getenv('API_KEY_JMESPATH', '<not set>')}")
114
+ logger.debug(f"API_KEY: {os.getenv('API_KEY', '<not set>')[:5] + '...' if os.getenv('API_KEY') else '<not set>'}")
115
+ logger.debug(f"STRIP_PARAM: {os.getenv('STRIP_PARAM', '<not set>')}")
117
116
  if not function_name:
118
117
  logger.error("function_name is empty or None")
119
118
  return json.dumps({"error": "function_name is required"})
@@ -159,8 +158,10 @@ def call_function(*, function_name: str, parameters: dict = None, env_key: str =
159
158
 
160
159
  operation = function_def["operation"]
161
160
  operation["method"] = function_def["method"]
162
- parameters = handle_custom_auth(operation, parameters)
163
- logger.debug(f"Parameters after auth handling: {parameters}")
161
+ headers = handle_auth(operation)
162
+ parameters = strip_parameters(parameters)
163
+ if function_def["method"] != "GET":
164
+ headers["Content-Type"] = "application/json"
164
165
 
165
166
  if not is_tool_whitelisted(function_def["path"]):
166
167
  logger.error(f"Access to function '{function_name}' is not allowed.")
@@ -173,14 +174,9 @@ def call_function(*, function_name: str, parameters: dict = None, env_key: str =
173
174
 
174
175
  path = function_def["path"]
175
176
  api_url = f"{base_url.rstrip('/')}/{path.lstrip('/')}"
176
- headers = {}
177
- if function_def["method"] != "GET":
178
- headers["Content-Type"] = "application/json"
179
- headers.update(get_auth_headers(spec))
180
177
  request_params = {}
181
178
  request_body = None
182
179
 
183
- # Map all path parameters dynamically from the spec
184
180
  if isinstance(parameters, dict):
185
181
  path_params_in_openapi = [
186
182
  param["name"] for param in operation.get("parameters", []) if param.get("in") == "path"
@@ -197,7 +193,6 @@ def call_function(*, function_name: str, parameters: dict = None, env_key: str =
197
193
  if param_name in parameters:
198
194
  api_url = api_url.replace(f"{{{param_name}}}", str(parameters.pop(param_name)))
199
195
  logger.debug(f"Replaced path param {param_name} in URL: {api_url}")
200
- # Remaining parameters go to query (GET) or body (non-GET)
201
196
  if function_def["method"] == "GET":
202
197
  request_params = parameters
203
198
  else:
@@ -15,12 +15,12 @@ from mcp import types
15
15
  from mcp.server.lowlevel import Server
16
16
  from mcp.server.models import InitializationOptions
17
17
  from mcp.server.stdio import stdio_server
18
- from mcp_openapi_proxy.utils import setup_logging, normalize_tool_name, is_tool_whitelisted, fetch_openapi_spec, get_auth_headers, detect_response_type, build_base_url, handle_custom_auth
18
+ from mcp_openapi_proxy.utils import setup_logging, normalize_tool_name, is_tool_whitelisted, fetch_openapi_spec, build_base_url, handle_auth, strip_parameters, detect_response_type
19
19
 
20
20
  DEBUG = os.getenv("DEBUG", "").lower() in ("true", "1", "yes")
21
21
  logger = setup_logging(debug=DEBUG)
22
22
 
23
- tools: List[types.Tool] = []
23
+ tools: List[types.Tool] = [] # Global tools list
24
24
  openapi_spec_data = None
25
25
 
26
26
  mcp = Server("OpenApiProxy-LowLevel")
@@ -31,8 +31,8 @@ async def dispatcher_handler(request: types.CallToolRequest) -> types.ServerResu
31
31
  try:
32
32
  function_name = request.params.name
33
33
  logger.debug(f"Dispatcher received CallToolRequest for function: {function_name}")
34
- logger.debug(f"API_KEY from env: {os.getenv('API_KEY', '<not set>')[:5] + '...' if os.getenv('API_KEY') else '<not set>'}")
35
- logger.debug(f"API_KEY_JMESPATH from env: {os.getenv('API_KEY_JMESPATH', '<not set>')}")
34
+ logger.debug(f"API_KEY: {os.getenv('API_KEY', '<not set>')[:5] + '...' if os.getenv('API_KEY') else '<not set>'}")
35
+ logger.debug(f"STRIP_PARAM: {os.getenv('STRIP_PARAM', '<not set>')}")
36
36
  tool = next((tool for tool in tools if tool.name == function_name), None)
37
37
  if not tool:
38
38
  logger.error(f"Unknown function requested: {function_name}")
@@ -42,7 +42,7 @@ async def dispatcher_handler(request: types.CallToolRequest) -> types.ServerResu
42
42
  )
43
43
  )
44
44
  arguments = request.params.arguments or {}
45
- logger.debug(f"Raw arguments before auth: {arguments}")
45
+ logger.debug(f"Raw arguments before processing: {arguments}")
46
46
 
47
47
  operation_details = lookup_operation_details(function_name, openapi_spec_data)
48
48
  if not operation_details:
@@ -55,11 +55,13 @@ async def dispatcher_handler(request: types.CallToolRequest) -> types.ServerResu
55
55
 
56
56
  operation = operation_details['operation']
57
57
  operation['method'] = operation_details['method']
58
- parameters = handle_custom_auth(operation, arguments)
59
- logger.debug(f"Parameters after auth handling: {parameters}")
58
+ headers = handle_auth(operation)
59
+ parameters = strip_parameters(arguments)
60
+ method = operation_details['method']
61
+ if method != "GET":
62
+ headers["Content-Type"] = "application/json"
60
63
 
61
64
  path = operation_details['path']
62
- method = operation_details['method']
63
65
 
64
66
  base_url = build_base_url(openapi_spec_data)
65
67
  if not base_url:
@@ -71,14 +73,9 @@ async def dispatcher_handler(request: types.CallToolRequest) -> types.ServerResu
71
73
  )
72
74
 
73
75
  api_url = f"{base_url.rstrip('/')}/{path.lstrip('/')}"
74
- headers = {}
75
- if method != "GET":
76
- headers["Content-Type"] = "application/json"
77
- headers.update(get_auth_headers(openapi_spec_data))
78
76
  request_params = {}
79
77
  request_body = None
80
78
 
81
- # Map all path parameters dynamically from the spec
82
79
  if isinstance(parameters, dict):
83
80
  path_params_in_openapi = [
84
81
  param['name'] for param in operation.get('parameters', []) if param.get('in') == 'path'
@@ -99,7 +96,6 @@ async def dispatcher_handler(request: types.CallToolRequest) -> types.ServerResu
99
96
  if param_name in parameters:
100
97
  api_url = api_url.replace(f"{{{param_name}}}", str(parameters.pop(param_name)))
101
98
  logger.debug(f"Replaced path param {param_name} in URL: {api_url}")
102
- # Remaining parameters go to query (GET) or body (non-GET)
103
99
  if method == "GET":
104
100
  request_params = parameters
105
101
  else:
@@ -158,7 +154,11 @@ async def list_tools(request: types.ListToolsRequest) -> types.ServerResult:
158
154
  return result
159
155
 
160
156
  def register_functions(spec: Dict) -> List[types.Tool]:
157
+ """Register tools from OpenAPI spec, preserving across calls if already populated."""
161
158
  global tools
159
+ if tools: # If tools already exist, don’t reset unless spec changes
160
+ logger.debug("Tools already registered, skipping re-registration")
161
+ return tools
162
162
  tools = []
163
163
  if not spec:
164
164
  logger.error("OpenAPI spec is None or empty.")
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Utility functions for mcp_openapi_proxy, including logging setup,
3
3
  OpenAPI fetching, name normalization, whitelist filtering, auth handling,
4
- and response type detection.
4
+ parameter stripping, and response type detection.
5
5
  """
6
6
 
7
7
  import os
@@ -11,7 +11,6 @@ import requests
11
11
  import re
12
12
  import json
13
13
  import yaml
14
- import jmespath
15
14
  from urllib.parse import urlparse
16
15
  from dotenv import load_dotenv
17
16
  from mcp import types
@@ -151,84 +150,33 @@ def fetch_openapi_spec(spec_url: str) -> dict:
151
150
  logger.error(f"Error parsing OpenAPI spec from {spec_url}: {e}")
152
151
  return None
153
152
 
154
- def get_auth_headers(spec: dict, api_key_env: str = "API_KEY") -> dict:
153
+ def handle_auth(operation: dict) -> dict:
154
+ """
155
+ Handles authentication for API requests.
156
+ - API_KEY: Sets Bearer header.
157
+ Returns: headers
158
+ """
159
+ api_key = os.getenv("API_KEY")
155
160
  headers = {}
156
- auth_token = os.getenv(api_key_env)
157
- if not auth_token:
158
- logger.debug(f"No {api_key_env} set, skipping auth headers.")
159
- return headers
160
- auth_type_override = os.getenv("API_AUTH_TYPE")
161
- if auth_type_override:
162
- headers["Authorization"] = f"{auth_type_override} {auth_token}"
163
- logger.debug(f"Using API_AUTH_TYPE override: Authorization: {auth_type_override} {redact_api_key(auth_token)}")
164
- return headers
165
- security_defs = spec.get('securityDefinitions', {})
166
- for name, definition in security_defs.items():
167
- if definition.get('type') == 'apiKey' and definition.get('in') == 'header' and definition.get('name') == 'Authorization':
168
- desc = definition.get('description', '')
169
- match = re.search(r'(\w+(?:-\w+)*)\s+<token>', desc)
170
- if match:
171
- prefix = match.group(1)
172
- headers["Authorization"] = f"{prefix} {auth_token}"
173
- logger.debug(f"Using apiKey with prefix from spec description: Authorization: {prefix} {redact_api_key(auth_token)}")
174
- else:
175
- headers["Authorization"] = auth_token
176
- logger.debug(f"Using raw apiKey auth from spec: Authorization: {redact_api_key(auth_token)}")
177
- return headers
178
- elif definition.get('type') == 'oauth2':
179
- headers["Authorization"] = f"Bearer {auth_token}"
180
- logger.debug(f"Using Bearer auth from spec: Authorization: Bearer {redact_api_key(auth_token)}")
181
- return headers
182
- headers["Authorization"] = auth_token
183
- logger.warning(f"No clear auth type in spec, using raw API key: Authorization: {redact_api_key(auth_token)}")
161
+ if api_key:
162
+ headers["Authorization"] = f"Bearer {api_key}"
163
+ logger.debug(f"Using API_KEY as Bearer: {redact_api_key(api_key)}")
164
+ logger.debug(f"Headers after auth: {headers}")
184
165
  return headers
185
166
 
186
- def handle_custom_auth(operation: dict, parameters: dict = None) -> dict:
167
+ def strip_parameters(parameters: dict = None) -> dict:
168
+ """
169
+ Strips specified parameter from parameters if STRIP_PARAM is set.
170
+ Returns: updated_parameters
171
+ """
187
172
  if parameters is None:
188
173
  parameters = {}
189
- logger.debug(f"Raw parameters before auth handling: {parameters}")
190
- api_key = os.getenv("API_KEY")
191
- jmespath_expr = os.getenv("API_KEY_JMESPATH")
192
- if not api_key or not jmespath_expr:
193
- logger.debug("No API_KEY or API_KEY_JMESPATH set, skipping custom auth handling.")
194
- return parameters
195
-
196
- method = operation.get("method", "GET").upper()
197
- request_data = {"query": {}, "body": {}}
198
- # Preserve original params, split by method
199
- if parameters:
200
- for key, value in parameters.items():
201
- param_in = next((p.get("in") for p in operation.get("parameters", []) if p.get("name") == key), None)
202
- if param_in == "query" or (method == "GET" and param_in not in ["path", "header"]):
203
- request_data["query"][key] = value
204
- elif param_in == "header":
205
- request_data["body"][key] = value
206
- elif param_in != "path": # Path params handled elsewhere
207
- request_data["body"][key] = value
208
-
209
- # Apply API_KEY via JMESPath
210
- try:
211
- if jmespath_expr:
212
- parts = jmespath_expr.split(".")
213
- target = request_data[parts[0]] if parts[0] in request_data else {}
214
- current = target
215
- for i, part in enumerate(parts[1:], 1):
216
- if i == len(parts) - 1:
217
- current[part] = api_key # Overwrite at final key
218
- else:
219
- current = current.setdefault(part, {})
220
- if parts[0] in request_data:
221
- request_data[parts[0]] = target
222
- logger.debug(f"Applied API_KEY to {jmespath_expr}: {redact_api_key(api_key)}")
223
- except Exception as e:
224
- logger.error(f"Error applying JMESPath expression {jmespath_expr}: {e}")
225
-
226
- # Merge back, overwriting original params
227
- if method == "GET":
228
- parameters = {**parameters, **request_data["query"]}
229
- else:
230
- parameters = {**parameters, **request_data["body"]}
231
- logger.debug(f"Parameters after custom auth merge: {parameters}")
174
+ logger.debug(f"Raw parameters before stripping: {parameters}")
175
+ strip_param = os.getenv("STRIP_PARAM")
176
+ if strip_param and isinstance(parameters, dict) and strip_param in parameters:
177
+ del parameters[strip_param]
178
+ logger.debug(f"Stripped param '{strip_param}' from parameters")
179
+ logger.debug(f"Parameters after stripping: {parameters}")
232
180
  return parameters
233
181
 
234
182
  def map_schema_to_tools(schema: dict) -> list:
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mcp-openapi-proxy
3
- Version: 0.1.1741477332
3
+ Version: 0.1.1741479690
4
4
  Summary: MCP server for exposing OpenAPI specifications as MCP tools.
5
5
  Author-email: Matthew Hand <matthewhandau@gmail.com>
6
+ Requires-Python: >=3.10
6
7
  Description-Content-Type: text/markdown
7
8
  License-File: LICENSE
8
9
  Requires-Dist: mcp[cli]>=1.2.0
@@ -197,9 +198,8 @@ Update your configuration:
197
198
  "env": {
198
199
  "OPENAPI_SPEC_URL": "https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json",
199
200
  "TOOL_WHITELIST": "/chat,/bots,/conversations,/reminders,/files,/users",
200
- "API_KEY": "<your_slack_bot_token>",
201
- "API_KEY_JMESPATH": "token",
202
- "TOOL_NAME_PREFIX": "slack_"
201
+ "API_KEY": "<your_slack_bot_token, starts with xoxb>",
202
+ "STRIP_PARAM": "token"
203
203
  }
204
204
  }
205
205
  }
@@ -234,7 +234,7 @@ Try these commands in your MCP client:
234
234
 
235
235
  ### GetZep Example
236
236
 
237
- ![image](https://github.com/user-attachments/assets/6ae7f708-9494-41a1-9075-e685f2cd8873)
237
+ ![image](https://github.com/user-attachments/assets/9a4fdabb-fa3d-4626-a50f-438147eadc9f)
238
238
 
239
239
  GetZep offers a free cloud API for memory management with detailed endpoints. Since GetZep did not provide an official OpenAPI specification, this project includes a generated spec hosted on GitHub for convenience. This approach—creating a spec from documentation—is a reusable pattern: users can similarly generate OpenAPI specs for any REST API and reference them locally (e.g., `file:///path/to/spec.json`). Obtain an API key from [GetZep's documentation](https://docs.getzep.com/).
240
240
 
@@ -4,7 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mcp-openapi-proxy"
7
- version = "0.1.1741477332"
7
+ requires-python = ">=3.10"
8
+ version = "0.1.1741479690"
8
9
  description = "MCP server for exposing OpenAPI specifications as MCP tools."
9
10
  readme = "README.md"
10
11
  authors = [
@@ -27,6 +28,7 @@ mcp-openapi-proxy = "mcp_openapi_proxy:main" # Correct entry pointing to __init
27
28
  [dependency-groups]
28
29
  dev = [
29
30
  "pytest>=8.3.4",
31
+ "pytest-asyncio>=0.21.0",
30
32
  ]
31
33
 
32
34
  [tool.pytest.ini_options]