Skip to content

Commit 36a637b

Browse files
authored
fix/struggle in serializing exceptions (#57)
* fix: skip serialization of exceptions * fix: minor
1 parent 17836bd commit 36a637b

File tree

3 files changed

+43
-11
lines changed

3 files changed

+43
-11
lines changed

src/mcp_scan/MCPScanner.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,29 +128,33 @@ async def get_servers_from_path(self, path: str) -> ScanPathResult:
128128

129129
async def check_server_changed(self, server: ServerScanResult) -> ServerScanResult:
130130
logger.debug("Checking for changes in server: %s", server.name)
131-
result = server.model_copy(deep=True)
131+
output_server = server.clone()
132+
if output_server.result is None:
133+
raise ValueError("Server result is None, cannot check for changes")
132134
for i, (entity, entity_result) in enumerate(server.entities_with_result):
133135
if entity_result is None:
134136
continue
135137
c, messages = self.storage_file.check_and_update(server.name or "", entity, entity_result.verified)
136-
result.result[i].changed = c
138+
output_server.result[i].changed = c
137139
if c:
138140
logger.info("Entity %s in server %s has changed", entity.name, server.name)
139-
result.result[i].messages.extend(messages)
140-
return result
141+
output_server.result[i].messages.extend(messages)
142+
return output_server
141143

142144
async def check_whitelist(self, server: ServerScanResult) -> ServerScanResult:
143145
logger.debug("Checking whitelist for server: %s", server.name)
144-
result = server.model_copy()
146+
output_server = server.clone()
147+
if output_server.result is None:
148+
raise ValueError("Server result is None, cannot check for changes")
145149
for i, (entity, entity_result) in enumerate(server.entities_with_result):
146150
if entity_result is None:
147151
continue
148152
if self.storage_file.is_whitelisted(entity):
149153
logger.debug("Entity %s is whitelisted", entity.name)
150-
result.result[i].whitelisted = True
154+
output_server.result[i].whitelisted = True
151155
else:
152-
result.result[i].whitelisted = False
153-
return result
156+
output_server.result[i].whitelisted = False
157+
return output_server
154158

155159
async def emit(self, signal: str, data: Any):
156160
logger.debug("Emitting signal: %s", signal)
@@ -159,7 +163,7 @@ async def emit(self, signal: str, data: Any):
159163

160164
async def scan_server(self, server: ServerScanResult, inspect_only: bool = False) -> ServerScanResult:
161165
logger.info("Scanning server: %s, inspect_only: %s", server.name, inspect_only)
162-
result = server.model_copy(deep=True)
166+
result = server.clone()
163167
try:
164168
result.signature = await check_server_with_timeout(
165169
server.server, self.server_timeout, self.suppress_mcpserver_io

src/mcp_scan/models.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ def serialize_exception(self, exception: Exception | None, _info) -> str | None:
129129
def text(self) -> str:
130130
return self.message or (str(self.exception) or "")
131131

132+
def clone(self) -> "ScanError":
133+
"""
134+
Create a copy of the ScanError instance. This is not the same as `model_copy(deep=True)`, because it does not
135+
clone the exception. This is crucial to avoid issues with serialization of exceptions.
136+
"""
137+
return ScanError(
138+
message=self.message,
139+
exception=self.exception,
140+
)
141+
132142

133143
class EntityScanResult(BaseModel):
134144
model_config = ConfigDict()
@@ -190,6 +200,15 @@ def entities_with_result(self) -> list[tuple[Entity, EntityScanResult | None]]:
190200
else:
191201
return [(entity, None) for entity in self.entities]
192202

203+
def clone(self) -> "ServerScanResult":
204+
"""
205+
Create a copy of the ServerScanResult instance. This is not the same as `model_copy(deep=True)`, because it does not
206+
clone the error. This is crucial to avoid issues with serialization of exceptions.
207+
"""
208+
output = self.model_copy(deep=True, update={"error": None})
209+
output.error = self.error.clone() if self.error else None
210+
return output
211+
193212

194213
class ScanPathResult(BaseModel):
195214
model_config = ConfigDict()
@@ -202,6 +221,15 @@ class ScanPathResult(BaseModel):
202221
def entities(self) -> list[Entity]:
203222
return list(chain.from_iterable(server.entities for server in self.servers))
204223

224+
def clone(self) -> "ScanPathResult":
225+
"""
226+
Create a copy of the ScanPathResult instance. This is not the same as `model_copy(deep=True)`, because it does not
227+
clone the error. This is crucial to avoid issues with serialization of exceptions.
228+
"""
229+
output = self.model_copy(deep=True, update={"error": None})
230+
output.error = self.error.clone() if self.error else None
231+
return output
232+
205233

206234
def entity_to_tool(
207235
entity: Entity,

src/mcp_scan/verify_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020

2121
async def verify_scan_path_public_api(scan_path: ScanPathResult, base_url: str) -> ScanPathResult:
22-
output_path = scan_path.model_copy(deep=True)
22+
output_path = scan_path.clone()
2323
url = base_url[:-1] if base_url.endswith("/") else base_url
2424
url = url + "/api/v1/public/mcp-scan"
2525
headers = {"Content-Type": "application/json"}
@@ -64,7 +64,7 @@ def get_policy() -> str:
6464

6565

6666
async def verify_scan_path_locally(scan_path: ScanPathResult) -> ScanPathResult:
67-
output_path = scan_path.model_copy(deep=True)
67+
output_path = scan_path.clone()
6868
tools_to_scan: list[Tool] = []
6969
for server in scan_path.servers:
7070
# None server signature are servers which are not reachable.

0 commit comments

Comments
 (0)