@@ -94,25 +94,33 @@ async def list_skills(self, params: ListSkillsParams | None = None) -> list[Skil
9494
9595 Raises:
9696 ValueError: If the server doesn't advertise the Skills extension, its
97- response is not SEP-2640 conformant, or it repeats a pagination cursor.
97+ response is not SEP-2640 conformant, it repeats a pagination
98+ cursor, or it lists the same skill URI on two pages.
9899 MCPError: If the server returns an error response.
99100 """
100101 self ._require_extension ()
101102 base = params if params is not None else ListSkillsParams ()
102103 cursor = base .cursor
103104 skills : list [Skill ] = []
104105 seen_cursors : set [str ] = {cursor } if cursor is not None else set ()
106+ seen_uris : set [str ] = set ()
105107 while True :
106108 # `send_request` parses each page into `ListSkillsResult`, whose validators reject a
107109 # non-conformant skill or a duplicate URI — no separate conformance call is needed.
108110 page = await self ._session .send_request (
109111 ListSkillsRequest (params = base .model_copy (update = {"cursor" : cursor })), ListSkillsResult
110112 )
113+ # A server stuck repeating its cursor is a loop; catch that before the content checks.
114+ if page .next_cursor is not None and page .next_cursor in seen_cursors :
115+ raise ValueError (f"server repeated skills/list pagination cursor { page .next_cursor !r} " )
116+ # Per-page validation can't catch a URI repeated *across* pages, so track that here.
117+ for skill in page .skills :
118+ if skill .uri in seen_uris :
119+ raise ValueError (f"server listed skill { skill .uri !r} on more than one skills/list page" )
120+ seen_uris .add (skill .uri )
111121 skills .extend (page .skills )
112122 if page .next_cursor is None :
113123 return skills
114- if page .next_cursor in seen_cursors :
115- raise ValueError (f"server repeated skills/list pagination cursor { page .next_cursor !r} " )
116124 seen_cursors .add (page .next_cursor )
117125 cursor = page .next_cursor
118126
@@ -156,25 +164,32 @@ async def read_directory(self, uri: str, params: ReadDirectoryParams | None = No
156164
157165 Raises:
158166 ValueError: If the server doesn't advertise the `directoryRead`
159- setting, its response is not a valid child listing of `uri`, or
160- it repeats a pagination cursor.
167+ setting, its response is not a valid child listing of `uri`, it
168+ repeats a pagination cursor, or it lists the same child on two pages .
161169 MCPError: If the server returns an error response.
162170 """
163171 self ._require_extension (directory_read = True )
164172 base = params if params is not None else ReadDirectoryParams (uri = uri )
165173 cursor = base .cursor
166174 resources : list [Resource ] = []
167175 seen_cursors : set [str ] = {cursor } if cursor is not None else set ()
176+ seen_uris : set [str ] = set ()
168177 while True :
169178 page = await self ._session .send_request (
170179 ReadDirectoryRequest (params = base .model_copy (update = {"uri" : uri , "cursor" : cursor })),
171180 ReadDirectoryResult ,
172181 )
182+ # A server stuck repeating its cursor is a loop; catch that before the content checks.
183+ if page .next_cursor is not None and page .next_cursor in seen_cursors :
184+ raise ValueError (f"server repeated resources/directory/read pagination cursor { page .next_cursor !r} " )
173185 validate_directory_result (uri , page )
186+ # `validate_directory_result` dedupes within a page; catch a child repeated across pages.
187+ for resource in page .resources :
188+ if resource .uri in seen_uris :
189+ raise ValueError (f"server listed child { resource .uri !r} on more than one directory page" )
190+ seen_uris .add (resource .uri )
174191 resources .extend (page .resources )
175192 if page .next_cursor is None :
176193 return resources
177- if page .next_cursor in seen_cursors :
178- raise ValueError (f"server repeated resources/directory/read pagination cursor { page .next_cursor !r} " )
179194 seen_cursors .add (page .next_cursor )
180195 cursor = page .next_cursor
0 commit comments