modelscope-hub 0.4.2__tar.gz → 0.4.4__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 (82) hide show
  1. {modelscope_hub-0.4.2/src/modelscope_hub.egg-info → modelscope_hub-0.4.4}/PKG-INFO +1 -1
  2. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_legacy_api.py +86 -6
  3. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_openapi.py +85 -3
  4. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_upload.py +769 -88
  5. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/agent/_api.py +90 -6
  6. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/agent_idp.py +35 -3
  7. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/api.py +32 -12
  8. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/agent.py +16 -0
  9. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/mcp.py +1 -2
  10. modelscope_hub-0.4.4/src/modelscope_hub/compat/constants.py +84 -0
  11. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/constants.py +361 -10
  12. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/version.py +1 -1
  13. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4/src/modelscope_hub.egg-info}/PKG-INFO +1 -1
  14. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/SOURCES.txt +5 -0
  15. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_agent_idp.py +15 -0
  16. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_agent_idp_keys.py +33 -0
  17. modelscope_hub-0.4.4/tests/test_commit_action_limit.py +110 -0
  18. modelscope_hub-0.4.4/tests/test_compat_constants_completeness.py +88 -0
  19. modelscope_hub-0.4.4/tests/test_repo_tree_page_retry.py +143 -0
  20. modelscope_hub-0.4.4/tests/test_upload_batching.py +885 -0
  21. modelscope_hub-0.4.4/tests/test_upload_blob_dedup.py +256 -0
  22. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_upload_config.py +140 -0
  23. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_upload_lfs_gate.py +7 -12
  24. modelscope_hub-0.4.2/src/modelscope_hub/compat/constants.py +0 -87
  25. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/LICENSE +0 -0
  26. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/README.md +0 -0
  27. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/pyproject.toml +0 -0
  28. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/setup.cfg +0 -0
  29. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/__init__.py +0 -0
  30. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_cache_manager.py +0 -0
  31. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_download.py +0 -0
  32. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_git.py +0 -0
  33. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/_repository.py +0 -0
  34. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/agent/__init__.py +0 -0
  35. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/__init__.py +0 -0
  36. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/agent_idp.py +0 -0
  37. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/base.py +0 -0
  38. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/cache.py +0 -0
  39. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/compat.py +0 -0
  40. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/deploy.py +0 -0
  41. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/download.py +0 -0
  42. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/login.py +0 -0
  43. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/main.py +0 -0
  44. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/repo.py +0 -0
  45. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/secret.py +0 -0
  46. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/studio.py +0 -0
  47. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/cli/upload.py +0 -0
  48. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/__init__.py +0 -0
  49. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/file_download.py +0 -0
  50. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/hub_api.py +0 -0
  51. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/compat/snapshot_download.py +0 -0
  52. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/config.py +0 -0
  53. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/errors.py +0 -0
  54. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/py.typed +0 -0
  55. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/types.py +0 -0
  56. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/__init__.py +0 -0
  57. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/file_utils.py +0 -0
  58. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/format.py +0 -0
  59. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/logger.py +0 -0
  60. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/media.py +0 -0
  61. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/patterns.py +0 -0
  62. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub/utils/time_utils.py +0 -0
  63. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/dependency_links.txt +0 -0
  64. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/entry_points.txt +0 -0
  65. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/requires.txt +0 -0
  66. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/src/modelscope_hub.egg-info/top_level.txt +0 -0
  67. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_cache_verification.py +0 -0
  68. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_compat_delete_files.py +0 -0
  69. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_compat_get_model_files.py +0 -0
  70. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_compat_snapshot_download.py +0 -0
  71. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_compat_studio.py +0 -0
  72. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_config_token.py +0 -0
  73. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_credential_lifecycle.py +0 -0
  74. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_download_lock_path.py +0 -0
  75. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_errors_openapi_codes.py +0 -0
  76. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_legacy_cache_detection.py +0 -0
  77. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_login_failure_paths.py +0 -0
  78. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_login_scoped_token.py +0 -0
  79. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_openapi_coverage.py +0 -0
  80. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_repo_files_truncation.py +0 -0
  81. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_user_info.py +0 -0
  82. {modelscope_hub-0.4.2 → modelscope_hub-0.4.4}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelscope-hub
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: The official Python client to connect with ModelScope Hub.
5
5
  License: Apache-2.0
6
6
  Keywords: modelscope,hub,sdk,openapi,machine-learning
@@ -14,6 +14,7 @@ needed for API calls.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import time
17
18
  import uuid
18
19
  from concurrent.futures import ThreadPoolExecutor, as_completed
19
20
  from typing import IO, Any, BinaryIO
@@ -24,18 +25,28 @@ from requests.adapters import HTTPAdapter, Retry
24
25
 
25
26
  from .constants import (
26
27
  API_CONNECT_TIMEOUT,
28
+ API_CONNECTION_POOL_MAXSIZE,
27
29
  API_MAX_RETRIES,
28
30
  API_TIMEOUT,
29
31
  LEGACY_API_PREFIX,
30
32
  REPO_FILES_TRUNCATION_LIMIT,
31
33
  REPO_TREE_MAX_REQUESTS,
34
+ REPO_TREE_PAGE_MAX_ATTEMPTS,
35
+ REPO_TREE_PAGE_RETRY_MAX_DELAY_SECONDS,
32
36
  REPO_TREE_WALK_WORKERS,
33
37
  UPLOAD_BLOB_CONNECT_TIMEOUT_SECONDS,
34
38
  UPLOAD_BLOB_READ_TIMEOUT_SECONDS,
35
39
  UPLOAD_HTTP_RETRY_ALLOWED_METHODS,
36
40
  RepoType,
37
41
  )
38
- from .errors import InvalidParameter, NetworkError, RequestTimeoutError, ServerError, raise_for_status
42
+ from .errors import (
43
+ InvalidParameter,
44
+ NetworkError,
45
+ PermissionDeniedError,
46
+ RequestTimeoutError,
47
+ ServerError,
48
+ raise_for_status,
49
+ )
39
50
  from .utils.logger import get_logger
40
51
 
41
52
  logger = get_logger("legacy_api")
@@ -100,6 +111,10 @@ class LegacyClient:
100
111
  self._endpoint = endpoint.rstrip("/")
101
112
  self._timeout: int | tuple[int, int] = (API_CONNECT_TIMEOUT, timeout)
102
113
  self._session_authenticated = False
114
+ # Set once any file-tree read succeeds. From then on a 403 on a tree
115
+ # request cannot be an authorization result, so it is retried instead of
116
+ # aborting an enumeration that spans hundreds of requests.
117
+ self._tree_reads_ok = False
103
118
 
104
119
  self._session = requests.Session()
105
120
  if user_agent:
@@ -110,7 +125,11 @@ class LegacyClient:
110
125
  status_forcelist=[429, 500, 502, 503, 504],
111
126
  allowed_methods=UPLOAD_HTTP_RETRY_ALLOWED_METHODS,
112
127
  )
113
- adapter = HTTPAdapter(max_retries=retry)
128
+ adapter = HTTPAdapter(
129
+ max_retries=retry,
130
+ pool_connections=API_CONNECTION_POOL_MAXSIZE,
131
+ pool_maxsize=API_CONNECTION_POOL_MAXSIZE,
132
+ )
114
133
  self._session.mount("https://", adapter)
115
134
  self._session.mount("http://", adapter)
116
135
 
@@ -394,7 +413,12 @@ class LegacyClient:
394
413
  params["Root"] = root
395
414
 
396
415
  suffix = "repo/tree" if _is_dataset(repo_type) else "repo/files"
397
- resp = self._request("GET", f"{segment}/{repo_id}/{suffix}", params=params)
416
+ resp = self._request_repo_tree(
417
+ f"{segment}/{repo_id}/{suffix}",
418
+ params,
419
+ repo_id=repo_id,
420
+ authorized=self._tree_reads_ok,
421
+ )
398
422
  data = self._json_data(resp)
399
423
  if isinstance(data, list):
400
424
  return data
@@ -567,6 +591,13 @@ class LegacyClient:
567
591
  Datasets can have millions of files, so this method pages through
568
592
  ``GET /api/v1/datasets/{repo_id}/repo/tree`` with
569
593
  ``PageNumber``/``PageSize`` params.
594
+
595
+ A page occasionally answers ``403 无权访问该数据集`` on a repository the
596
+ caller demonstrably can read. Once any page has succeeded the credential
597
+ is proven, so a later-page denial is a server-side hiccup rather than an
598
+ authorization result, and it is retried instead of discarding every page
599
+ collected so far -- a large listing spans hundreds of pages, which makes
600
+ hitting it near-certain.
570
601
  """
571
602
  all_files: list[dict] = []
572
603
  page_number = 1
@@ -579,10 +610,11 @@ class LegacyClient:
579
610
  }
580
611
  if root_path and root_path != "/":
581
612
  params["Root"] = root_path
582
- resp = self._request(
583
- "GET",
613
+ resp = self._request_repo_tree(
584
614
  f"datasets/{repo_id}/repo/tree",
585
- params=params,
615
+ params,
616
+ repo_id=repo_id,
617
+ authorized=self._tree_reads_ok,
586
618
  )
587
619
  data = self._json_data(resp)
588
620
  if isinstance(data, list):
@@ -598,6 +630,54 @@ class LegacyClient:
598
630
  page_number += 1
599
631
  return all_files
600
632
 
633
+ def _request_repo_tree(
634
+ self,
635
+ path: str,
636
+ params: dict[str, Any],
637
+ *,
638
+ repo_id: str,
639
+ authorized: bool,
640
+ ) -> Any:
641
+ """Fetch one file-tree listing, retrying a spurious denial.
642
+
643
+ The server intermittently answers a tree request with ``403 无权访问该数据
644
+ 集`` on a repository the caller has just read successfully -- observed on
645
+ both ``PageNumber``-paginated and ``Root``-scoped listings. Enumerating a
646
+ large repository takes hundreds of such requests, so at that scale a
647
+ single-request failure rate is effectively a guaranteed whole-listing
648
+ failure, and it discards every entry gathered so far.
649
+
650
+ ``authorized`` means some tree read already succeeded on this client, so
651
+ the credential is proven and a denial cannot be an authorization result.
652
+ Until then a 403 is taken at face value, keeping a real permission error
653
+ fast and honest.
654
+ """
655
+ last_error: PermissionDeniedError | None = None
656
+ for attempt in range(REPO_TREE_PAGE_MAX_ATTEMPTS):
657
+ try:
658
+ resp = self._request("GET", path, params=params)
659
+ except PermissionDeniedError as error:
660
+ if not authorized:
661
+ raise
662
+ last_error = error
663
+ if attempt < REPO_TREE_PAGE_MAX_ATTEMPTS - 1:
664
+ wait = min(2**attempt, REPO_TREE_PAGE_RETRY_MAX_DELAY_SECONDS)
665
+ logger.warning(
666
+ "Repo %s: tree listing (%s) denied on an already-authorized repo, retrying in %ds ...",
667
+ repo_id,
668
+ params.get("Root") or params.get("PageNumber") or "/",
669
+ wait,
670
+ )
671
+ time.sleep(wait)
672
+ continue
673
+ self._tree_reads_ok = True
674
+ return resp
675
+ raise NetworkError(
676
+ f"Repo {repo_id}: tree listing ({params.get('Root') or params.get('PageNumber') or '/'}) kept "
677
+ f"returning a denial after {REPO_TREE_PAGE_MAX_ATTEMPTS} attempts on an already-authorized "
678
+ f"repo: {last_error}"
679
+ ) from last_error
680
+
601
681
  # ------------------------------------------------------------------
602
682
  # Revisions
603
683
  # ------------------------------------------------------------------
@@ -30,9 +30,17 @@ from typing import Any, BinaryIO
30
30
  from urllib.parse import urljoin, urlsplit
31
31
 
32
32
  import requests
33
+ from requests.adapters import HTTPAdapter
33
34
 
34
35
  from .config import HubConfig, get_default_config
35
- from .constants import API_CONNECT_TIMEOUT, API_MAX_RETRIES, API_TIMEOUT, OPENAPI_PREFIX, TokenScope
36
+ from .constants import (
37
+ API_CONNECT_TIMEOUT,
38
+ API_CONNECTION_POOL_MAXSIZE,
39
+ API_MAX_RETRIES,
40
+ API_TIMEOUT,
41
+ OPENAPI_PREFIX,
42
+ TokenScope,
43
+ )
36
44
  from .errors import (
37
45
  APIError,
38
46
  AuthenticationError,
@@ -200,6 +208,16 @@ class OpenAPIClient:
200
208
  ) -> None:
201
209
  self._config = config or get_default_config()
202
210
  self._session = session or requests.Session()
211
+ if session is None:
212
+ # A bare Session caps the pool at urllib3's default of 10, which is
213
+ # below the concurrency bulk transfers use; the excess connections
214
+ # are discarded and pay for a new TLS handshake on next use.
215
+ adapter = HTTPAdapter(
216
+ pool_connections=API_CONNECTION_POOL_MAXSIZE,
217
+ pool_maxsize=API_CONNECTION_POOL_MAXSIZE,
218
+ )
219
+ self._session.mount("https://", adapter)
220
+ self._session.mount("http://", adapter)
203
221
  self._timeout: float | tuple[float, float] = (
204
222
  float(timeout) if timeout is not None else (float(API_CONNECT_TIMEOUT), float(API_TIMEOUT))
205
223
  )
@@ -1084,6 +1102,62 @@ class OpenAPIClient:
1084
1102
  params.append((key, str(value).lower() if isinstance(value, bool) else str(value)))
1085
1103
  return params
1086
1104
 
1105
+ @staticmethod
1106
+ def _decode_mcp_list_response(response: requests.Response) -> JSON:
1107
+ """Decode MCP list replies, including legacy HTTP-200 error envelopes.
1108
+
1109
+ The service normally signals errors through HTTP status codes. Some
1110
+ deployments instead return ``200`` with ``success: false`` and an
1111
+ OpenAPI-style error code. Treating that dictionary as a list payload
1112
+ silently turned invalid request bodies into empty server lists.
1113
+ """
1114
+ try:
1115
+ payload = response.json()
1116
+ except ValueError:
1117
+ return {"raw_response": response.text}
1118
+ if not isinstance(payload, dict):
1119
+ return payload
1120
+
1121
+ success = payload.get("success") if "success" in payload else payload.get("Success")
1122
+ if success is False:
1123
+ code = payload.get("code") if payload.get("code") is not None else payload.get("Code")
1124
+ message = next(
1125
+ (
1126
+ value.strip()
1127
+ for key in ("message", "Message", "msg", "Msg", "detail", "Detail")
1128
+ if isinstance(value := payload.get(key), str) and value.strip()
1129
+ ),
1130
+ "MCP server list request was rejected.",
1131
+ )
1132
+ request_id = payload.get("request_id") or payload.get("requestId") or payload.get("RequestId")
1133
+ code_text = str(code).strip()
1134
+ error_cls: type[APIError] = APIError
1135
+ status_code = 400
1136
+ if code_text == "InputParameterError":
1137
+ error_cls = InvalidParameter
1138
+ elif code_text == "InvalidAuthentication":
1139
+ error_cls = AuthenticationError
1140
+ status_code = 401
1141
+ elif code_text == "OperationNotAllowed":
1142
+ error_cls = PermissionDeniedError
1143
+ status_code = 403
1144
+ elif code_text == "RateLimitExceed":
1145
+ error_cls = RateLimitError
1146
+ status_code = 429
1147
+ raise error_cls(
1148
+ message,
1149
+ status_code=status_code,
1150
+ request_id=request_id,
1151
+ response_body=payload,
1152
+ url=response.url,
1153
+ method=response.request.method if response.request else "PUT",
1154
+ )
1155
+ if "data" in payload:
1156
+ return payload["data"]
1157
+ if "Data" in payload:
1158
+ return payload["Data"]
1159
+ return payload
1160
+
1087
1161
  @staticmethod
1088
1162
  def _is_method_or_route_unsupported(exc: APIError) -> bool:
1089
1163
  return exc.status_code in (404, 405, 501)
@@ -1307,6 +1381,10 @@ class OpenAPIClient:
1307
1381
  filter : dict, optional
1308
1382
  Nested filter object. Supported keys: ``category``, ``is_hosted``.
1309
1383
  """
1384
+ if isinstance(page_number, bool) or not isinstance(page_number, int) or page_number < 1:
1385
+ raise InvalidParameter("page_number must be an integer >= 1.")
1386
+ if isinstance(page_size, bool) or not isinstance(page_size, int) or page_size < 1:
1387
+ raise InvalidParameter("page_size must be an integer >= 1.")
1310
1388
  if page_number * page_size > 100:
1311
1389
  # The service enforces this itself, answering 403 QuotaLimitExceed with
1312
1390
  # exactly this rule. Checking here spares the round trip and reports it
@@ -1326,14 +1404,16 @@ class OpenAPIClient:
1326
1404
  body = {k: v for k, v in body.items() if v is not None}
1327
1405
 
1328
1406
  def _get() -> JSON:
1329
- return self._request(
1407
+ response = self._request(
1330
1408
  "GET",
1331
1409
  "/mcp/servers",
1332
1410
  params=self._flatten_mcp_list_params(body),
1333
1411
  require_token=False,
1334
1412
  required_scope=TokenScope.READ,
1335
1413
  anonymous_retry=True,
1414
+ unwrap=False,
1336
1415
  )
1416
+ return self._decode_mcp_list_response(response)
1337
1417
 
1338
1418
  if self._mcp_list_supports_get:
1339
1419
  # This deployment already answered GET and refused PUT, so leading
@@ -1341,14 +1421,16 @@ class OpenAPIClient:
1341
1421
  return _get()
1342
1422
 
1343
1423
  try:
1344
- return self._request(
1424
+ response = self._request(
1345
1425
  "PUT",
1346
1426
  "/mcp/servers",
1347
1427
  json_body=body,
1348
1428
  require_token=False,
1349
1429
  required_scope=TokenScope.READ,
1350
1430
  anonymous_retry=True,
1431
+ unwrap=False,
1351
1432
  )
1433
+ return self._decode_mcp_list_response(response)
1352
1434
  except APIError as exc:
1353
1435
  if self._mcp_list_supports_get is False or not self._is_method_or_route_unsupported(exc):
1354
1436
  raise