code-analysis-client 1.6.178__tar.gz → 1.6.187__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 (27) hide show
  1. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/PKG-INFO +2 -2
  2. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/__init__.py +22 -0
  3. code_analysis_client-1.6.187/code_analysis_client/check_chain.py +350 -0
  4. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/client.py +86 -6
  5. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/exceptions.py +2 -0
  6. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/file_session.py +18 -1
  7. code_analysis_client-1.6.187/code_analysis_client/version.txt +1 -0
  8. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/PKG-INFO +2 -2
  9. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/SOURCES.txt +1 -0
  10. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/requires.txt +1 -1
  11. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/pyproject.toml +1 -1
  12. code_analysis_client-1.6.178/code_analysis_client/version.txt +0 -1
  13. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/README.md +0 -0
  14. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/commands_proxy.py +0 -0
  15. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/config.py +0 -0
  16. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/py.typed +0 -0
  17. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/queue_wait.py +0 -0
  18. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/responses.py +0 -0
  19. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/schema_disk_cache.py +0 -0
  20. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/server_api.py +0 -0
  21. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/server_schema.py +0 -0
  22. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/universal_file.py +0 -0
  23. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/validation.py +0 -0
  24. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/worktree.py +0 -0
  25. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/dependency_links.txt +0 -0
  26. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/top_level.txt +0 -0
  27. {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/setup.cfg +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.6.178
3
+ Version: 1.6.187
4
4
  Summary: Async JSON-RPC client for the code-analysis MCP server (mcp-proxy-adapter JsonRpcClient)
5
5
  Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
- Requires-Dist: mcp-proxy-adapter>=8.10.59
8
+ Requires-Dist: mcp-proxy-adapter>=8.10.69
9
9
  Provides-Extra: dev
10
10
  Requires-Dist: pytest>=8.0; extra == "dev"
11
11
  Requires-Dist: pytest-asyncio>=0.25.0; extra == "dev"
@@ -24,6 +24,18 @@ from code_analysis_client.exceptions import (
24
24
  QueueJobError,
25
25
  )
26
26
  from code_analysis_client.file_session import FileSessionClient, SessionNotFoundError
27
+ from code_analysis_client.check_chain import (
28
+ CheckChainClient,
29
+ CheckChainConfigInvalidError,
30
+ CheckChainError,
31
+ CheckChainGetResult,
32
+ CheckChainListResult,
33
+ ChainElement,
34
+ Degraded,
35
+ Diagnostic,
36
+ ExtensionChain,
37
+ FileCheckAndPromoteResult,
38
+ )
27
39
  from code_analysis_client.worktree import WorktreeClient, WorktreeConflictError
28
40
  from code_analysis_client.queue_wait import (
29
41
  QueuedJob,
@@ -72,6 +84,16 @@ __all__ = [
72
84
  "UNIVERSAL_FILE_COMMANDS",
73
85
  "UniversalFileClient",
74
86
  "ValidatedCommandsProxy",
87
+ "CheckChainClient",
88
+ "CheckChainConfigInvalidError",
89
+ "CheckChainError",
90
+ "CheckChainGetResult",
91
+ "CheckChainListResult",
92
+ "ChainElement",
93
+ "Degraded",
94
+ "Diagnostic",
95
+ "ExtensionChain",
96
+ "FileCheckAndPromoteResult",
75
97
  "WorktreeClient",
76
98
  "WorktreeConflictError",
77
99
  "adapter_settings_from_server_config",
@@ -0,0 +1,350 @@
1
+ """
2
+ Check-chain commands on top of CodeAnalysisAsyncClient.
3
+
4
+ Typed wrappers for ``check_chain_list``, ``check_chain_get`` and
5
+ ``file_check_and_promote`` with full parameter mapping and result/error mapping:
6
+ diagnostics, degraded outcomes, sync conflicts and the closed error enum of
7
+ ``file_check_and_promote``.
8
+
9
+ Author: Vasiliy Zdanovskiy
10
+ email: vasilyvz@gmail.com
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from dataclasses import dataclass, field
17
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
18
+
19
+ from code_analysis_client.exceptions import ClientValidationError
20
+ from code_analysis_client.responses import command_error_code
21
+ from code_analysis_client.worktree import WorktreeConflictError
22
+
23
+ if TYPE_CHECKING:
24
+ from code_analysis_client.client import CodeAnalysisAsyncClient
25
+
26
+ EXTENSION_PATTERN = r"^\.[A-Za-z0-9]+(\.[A-Za-z0-9]+)*$"
27
+ _EXTENSION_RE = re.compile(EXTENSION_PATTERN)
28
+
29
+ FCP_ERROR_CODES = frozenset(
30
+ {
31
+ "VALIDATION_ERROR",
32
+ "PROJECT_NOT_FOUND",
33
+ "PROJECT_LOCKED",
34
+ "SESSION_NOT_FOUND",
35
+ "WORKTREE_CREATE_FAILED",
36
+ "WORKTREE_CREATE_TIMEOUT",
37
+ "WORKTREE_SYNC_CONFLICT",
38
+ "WORKTREE_MERGE_IN_PROGRESS",
39
+ "WORKTREE_REBASE_IN_PROGRESS",
40
+ "FCP_WRITE_FAILED",
41
+ "CHECK_CHAIN_CONFIG_INVALID",
42
+ "FCP_CHECK_TOOL_CRASHED",
43
+ "FCP_CHECK_TOOL_TIMEOUT",
44
+ "WORKTREE_COMMIT_FAILED",
45
+ "FCP_PROMOTE_RACE",
46
+ "GIT_NOT_AVAILABLE",
47
+ "GIT_NOT_A_REPO",
48
+ }
49
+ )
50
+
51
+
52
+ class CheckChainError(ClientValidationError):
53
+ """A check-chain command answered with a server error.
54
+
55
+ Attributes:
56
+ code: the server error code (e.g. ``FCP_PROMOTE_RACE``).
57
+ details: the server error details/data object.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ message: str,
63
+ *,
64
+ code: str,
65
+ field: Optional[str] = None,
66
+ details: Optional[Dict[str, Any]] = None,
67
+ ) -> None:
68
+ """Store the server error code with the message and details."""
69
+ super().__init__(message, field=field or "command", details=details)
70
+ self.code = code
71
+
72
+
73
+ class CheckChainConfigInvalidError(CheckChainError):
74
+ """CHECK_CHAIN_CONFIG_INVALID with its section, extension and reason."""
75
+
76
+ def __init__(self, message: str, *, details: Optional[Dict[str, Any]] = None) -> None:
77
+ """Expose details.section/extension/reason as attributes."""
78
+ super().__init__(
79
+ message, code="CHECK_CHAIN_CONFIG_INVALID", field="check_chains", details=details
80
+ )
81
+ self.section = self.details.get("section")
82
+ self.extension = self.details.get("extension")
83
+ self.reason = self.details.get("reason")
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class ChainElement:
88
+ """One check-chain element."""
89
+
90
+ tool: str
91
+ config_source: str
92
+ blocking: bool
93
+ args: List[str] = field(default_factory=list)
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class ExtensionChain:
98
+ """Resolved chain of one extension."""
99
+
100
+ extension: str
101
+ source: str
102
+ chain: List[ChainElement]
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class CheckChainListResult:
107
+ """Result of ``check_chain_list``."""
108
+
109
+ project_id: str
110
+ chains: List[ExtensionChain]
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class CheckChainGetResult:
115
+ """Result of ``check_chain_get``."""
116
+
117
+ project_id: str
118
+ extension: str
119
+ source: str
120
+ chain: List[ChainElement]
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class Diagnostic:
125
+ """One finding of a chain element."""
126
+
127
+ tool: str
128
+ path: str
129
+ line: Optional[int]
130
+ column: Optional[int]
131
+ code: Optional[str]
132
+ severity: Optional[str]
133
+ message: str
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class Degraded:
138
+ """Why a call degraded (nothing promoted)."""
139
+
140
+ reason: str
141
+ tool: Optional[str] = None
142
+ skipped_tools: List[str] = field(default_factory=list)
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class FileCheckAndPromoteResult:
147
+ """Result of ``file_check_and_promote`` (outcome-discriminated).
148
+
149
+ ``outcome`` is ``promoted``, ``diagnostics`` or ``degraded``. ``promoted`` is
150
+ True only for ``promoted``. ``degraded`` may also be set on a promoted outcome
151
+ when only non-blocking tools were skipped (``skipped_tools``).
152
+ """
153
+
154
+ project_id: str
155
+ editor_session_id: str
156
+ path: str
157
+ outcome: str
158
+ promoted: bool
159
+ commit: Optional[str]
160
+ index_pending: bool
161
+ index_job_id: Optional[str]
162
+ index_trigger_error: Optional[str]
163
+ diagnostics: List[Diagnostic]
164
+ degraded: Optional[Degraded]
165
+ raw: Dict[str, Any] = field(default_factory=dict, compare=False, repr=False)
166
+
167
+
168
+ def _require_non_empty(value: Any, *, field_name: str) -> str:
169
+ """Return stripped text or raise ClientValidationError."""
170
+ text = str(value or "").strip()
171
+ if not text:
172
+ raise ClientValidationError(f"{field_name} is required", field=field_name)
173
+ return text
174
+
175
+
176
+ def _error_parts(data: Dict[str, Any]) -> tuple[str, str, Dict[str, Any]]:
177
+ """Return (code, message, details) of a failed response."""
178
+ code = command_error_code(data)
179
+ err = data.get("error")
180
+ err_dict: Dict[str, Any] = err if isinstance(err, dict) else {}
181
+ message = data.get("message") or err_dict.get("message") or code or str(data)
182
+ details = (
183
+ err_dict.get("details")
184
+ or err_dict.get("data")
185
+ or data.get("details")
186
+ or data.get("data")
187
+ or {}
188
+ )
189
+ return code, str(message), details if isinstance(details, dict) else {}
190
+
191
+
192
+ def _success_payload(data: Dict[str, Any], *, command: str) -> Dict[str, Any]:
193
+ """Return the success data dict, or raise the mapped error for a failure."""
194
+ if data.get("success"):
195
+ inner = data.get("data")
196
+ return inner if isinstance(inner, dict) else data
197
+ code, message, details = _error_parts(data)
198
+ if code == "WORKTREE_SYNC_CONFLICT":
199
+ raise WorktreeConflictError(
200
+ message,
201
+ conflicts=details.get("conflicts"),
202
+ preserved_locked_files=details.get("preserved_locked_files"),
203
+ )
204
+ if code == "CHECK_CHAIN_CONFIG_INVALID":
205
+ raise CheckChainConfigInvalidError(message, details=details)
206
+ raise CheckChainError(
207
+ message, code=code or "UNKNOWN", field=details.get("field") or command, details=details
208
+ )
209
+
210
+
211
+ def _element(raw: Any) -> ChainElement:
212
+ """Map one raw chain element."""
213
+ item = raw if isinstance(raw, dict) else {}
214
+ return ChainElement(
215
+ tool=str(item.get("tool", "")),
216
+ config_source=str(item.get("config_source", "auto")),
217
+ blocking=bool(item.get("blocking", False)),
218
+ args=[str(a) for a in (item.get("args") or [])],
219
+ )
220
+
221
+
222
+ def _diagnostic(raw: Any) -> Diagnostic:
223
+ """Map one raw diagnostic."""
224
+ item = raw if isinstance(raw, dict) else {}
225
+ return Diagnostic(
226
+ tool=str(item.get("tool", "")),
227
+ path=str(item.get("path", "")),
228
+ line=item.get("line"),
229
+ column=item.get("column"),
230
+ code=item.get("code"),
231
+ severity=item.get("severity"),
232
+ message=str(item.get("message", "")),
233
+ )
234
+
235
+
236
+ def _degraded(raw: Any) -> Optional[Degraded]:
237
+ """Map the degraded object (None when absent)."""
238
+ if not isinstance(raw, dict):
239
+ return None
240
+ return Degraded(
241
+ reason=str(raw.get("reason", "")),
242
+ tool=raw.get("tool"),
243
+ skipped_tools=[str(t) for t in (raw.get("skipped_tools") or [])],
244
+ )
245
+
246
+
247
+ class CheckChainClient:
248
+ """check_chain_list / check_chain_get / file_check_and_promote."""
249
+
250
+ __slots__ = ("_client",)
251
+
252
+ def __init__(self, client: CodeAnalysisAsyncClient) -> None:
253
+ """Store the underlying async client."""
254
+ self._client = client
255
+
256
+ async def list(self, project_id: str) -> CheckChainListResult:
257
+ """Return the resolved chain of every configured extension.
258
+
259
+ Calls ``check_chain_list``. Raises CheckChainConfigInvalidError for a
260
+ malformed registry entry and CheckChainError for other server errors.
261
+ """
262
+ project_id = _require_non_empty(project_id, field_name="project_id")
263
+ data = await self._client.call_validated(
264
+ "check_chain_list", {"project_id": project_id}
265
+ )
266
+ payload = _success_payload(data, command="check_chain_list")
267
+ return CheckChainListResult(
268
+ project_id=str(payload.get("project_id", project_id)),
269
+ chains=[
270
+ ExtensionChain(
271
+ extension=str(entry.get("extension", "")),
272
+ source=str(entry.get("source", "empty")),
273
+ chain=[_element(e) for e in (entry.get("chain") or [])],
274
+ )
275
+ for entry in (payload.get("chains") or [])
276
+ if isinstance(entry, dict)
277
+ ],
278
+ )
279
+
280
+ async def get(self, project_id: str, extension: str) -> CheckChainGetResult:
281
+ """Return the resolved chain of one extension (leading dot required).
282
+
283
+ Calls ``check_chain_get``. An unconfigured extension is source ``empty``
284
+ with an empty chain.
285
+ """
286
+ project_id = _require_non_empty(project_id, field_name="project_id")
287
+ extension = _require_non_empty(extension, field_name="extension")
288
+ if not _EXTENSION_RE.match(extension):
289
+ raise ClientValidationError(
290
+ f"extension must match {EXTENSION_PATTERN}", field="extension"
291
+ )
292
+ data = await self._client.call_validated(
293
+ "check_chain_get", {"project_id": project_id, "extension": extension}
294
+ )
295
+ payload = _success_payload(data, command="check_chain_get")
296
+ return CheckChainGetResult(
297
+ project_id=str(payload.get("project_id", project_id)),
298
+ extension=str(payload.get("extension", extension)),
299
+ source=str(payload.get("source", "empty")),
300
+ chain=[_element(e) for e in (payload.get("chain") or [])],
301
+ )
302
+
303
+ async def file_check_and_promote(
304
+ self,
305
+ project_id: str,
306
+ editor_session_id: str,
307
+ path: str,
308
+ content: str,
309
+ ) -> FileCheckAndPromoteResult:
310
+ """Write, check and promote one file through the editor's worktree.
311
+
312
+ Calls ``file_check_and_promote``. Diagnostics and degraded outcomes are
313
+ successful results (``outcome`` / ``promoted``), never exceptions.
314
+ Raises WorktreeConflictError on WORKTREE_SYNC_CONFLICT (with
315
+ ``conflicts`` and ``preserved_locked_files``), CheckChainConfigInvalidError
316
+ for a malformed registry entry, and CheckChainError (``code`` from the
317
+ closed enum, e.g. FCP_PROMOTE_RACE) for every other server error.
318
+ """
319
+ project_id = _require_non_empty(project_id, field_name="project_id")
320
+ editor_session_id = _require_non_empty(
321
+ editor_session_id, field_name="editor_session_id"
322
+ )
323
+ path = _require_non_empty(path, field_name="path")
324
+ if not isinstance(content, str):
325
+ raise ClientValidationError("content must be a string", field="content")
326
+ data = await self._client.call_validated(
327
+ "file_check_and_promote",
328
+ {
329
+ "project_id": project_id,
330
+ "editor_session_id": editor_session_id,
331
+ "path": path,
332
+ "content": content,
333
+ },
334
+ )
335
+ payload = _success_payload(data, command="file_check_and_promote")
336
+ outcome = str(payload.get("outcome", ""))
337
+ return FileCheckAndPromoteResult(
338
+ project_id=str(payload.get("project_id", project_id)),
339
+ editor_session_id=str(payload.get("editor_session_id", editor_session_id)),
340
+ path=str(payload.get("path", path)),
341
+ outcome=outcome,
342
+ promoted=bool(payload.get("promoted", outcome == "promoted")),
343
+ commit=payload.get("commit"),
344
+ index_pending=bool(payload.get("index_pending", False)),
345
+ index_job_id=payload.get("index_job_id"),
346
+ index_trigger_error=payload.get("index_trigger_error"),
347
+ diagnostics=[_diagnostic(d) for d in (payload.get("diagnostics") or [])],
348
+ degraded=_degraded(payload.get("degraded")),
349
+ raw=payload,
350
+ )
@@ -15,6 +15,7 @@ try:
15
15
  from mcp_proxy_adapter.client.jsonrpc_client.client import JsonRpcClient
16
16
  except Exception: # pragma: no cover - compatibility with older layouts
17
17
  from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient
18
+ from mcp_proxy_adapter.client.jsonrpc_client.exceptions import ClientRemoteResponseError
18
19
 
19
20
  from code_analysis_client.commands_proxy import ValidatedCommandsProxy
20
21
  from code_analysis_client.config import (
@@ -23,6 +24,7 @@ from code_analysis_client.config import (
23
24
  load_server_config,
24
25
  )
25
26
  from code_analysis_client.file_session import FileSessionClient
27
+ from code_analysis_client.check_chain import CheckChainClient
26
28
  from code_analysis_client.worktree import WorktreeClient
27
29
  from code_analysis_client.queue_wait import (
28
30
  QueuedJob,
@@ -39,12 +41,40 @@ from code_analysis_client.schema_disk_cache import (
39
41
  store_cached_schema,
40
42
  )
41
43
  from code_analysis_client.server_schema import fetch_command_schema_from_server
44
+ from code_analysis_client.exceptions import ClientValidationError
42
45
  from code_analysis_client.validation import (
43
46
  prepare_params_for_schema,
44
47
  validate_params_against_schema,
45
48
  )
46
49
 
47
50
 
51
+ def _remote_invalid_params_as_client_validation(
52
+ error: ClientRemoteResponseError,
53
+ ) -> ClientValidationError | None:
54
+ """Convert the adapter's JSON-RPC invalid-params response for client callers.
55
+
56
+ ``JsonRpcClient.execute_command()`` raises ``ClientRemoteResponseError``
57
+ for every top-level JSON-RPC error. Code-analysis validation errors use
58
+ the standard ``-32602`` code, however, and callers of this package already
59
+ handle local schema failures as ``ClientValidationError``. Preserve the
60
+ adapter's complete message and the server's validation payload while
61
+ presenting both local and remote invalid-parameter failures through that
62
+ one client contract.
63
+ """
64
+ details = error.details
65
+ if details.get("code") != -32602:
66
+ return None
67
+
68
+ payload = details.get("data")
69
+ payload = payload if isinstance(payload, dict) else {}
70
+ field = payload.get("field")
71
+ return ClientValidationError(
72
+ str(error),
73
+ field=field if isinstance(field, str) else "command",
74
+ details=payload,
75
+ )
76
+
77
+
48
78
  class CodeAnalysisAsyncClient:
49
79
  """Async client: domain commands via :meth:`call` / :meth:`call_unified`; full adapter API on :attr:`rpc`."""
50
80
 
@@ -153,6 +183,11 @@ class CodeAnalysisAsyncClient:
153
183
  """Worktree management: ``worktree_*`` commands."""
154
184
  return WorktreeClient(self)
155
185
 
186
+ @property
187
+ def check_chains(self) -> CheckChainClient:
188
+ """Check-chain registry and promote: ``check_chain_*``, ``file_check_and_promote``."""
189
+ return CheckChainClient(self)
190
+
156
191
  @property
157
192
  def universal_files(self) -> UniversalFileClient:
158
193
  """Read-only structured preview (``universal_file_preview`` command)."""
@@ -220,10 +255,16 @@ class CodeAnalysisAsyncClient:
220
255
 
221
256
  A non-queued response is returned unchanged regardless of ``auto_poll``.
222
257
  """
223
- resp = await self._rpc.execute_command(
224
- command,
225
- params or {},
226
- )
258
+ try:
259
+ resp = await self._rpc.execute_command(
260
+ command,
261
+ params or {},
262
+ )
263
+ except ClientRemoteResponseError as exc:
264
+ validation_error = _remote_invalid_params_as_client_validation(exc)
265
+ if validation_error is None:
266
+ raise
267
+ raise validation_error from exc
227
268
  if not is_queued_envelope(resp):
228
269
  return resp
229
270
 
@@ -240,6 +281,45 @@ class CodeAnalysisAsyncClient:
240
281
  )
241
282
  return await unwrap_job_result(status, rpc=self._rpc)
242
283
 
284
+ async def _execute_validated(
285
+ self,
286
+ command: str,
287
+ params: Dict[str, Any],
288
+ *,
289
+ use_cmd_endpoint: bool,
290
+ auto_poll: bool,
291
+ timeout: Optional[float],
292
+ poll_interval: float,
293
+ status_hook: Optional[StatusHook],
294
+ ) -> Union[Dict[str, Any], QueuedJob]:
295
+ """Execute after schema validation and reconcile a stale command catalog.
296
+
297
+ A cached schema can outlive a deployment that removes its command. If
298
+ execution reports JSON-RPC ``-32601``, discard the cached catalog view
299
+ and ask live ``help`` once. A command still present keeps the original
300
+ remote error; an absent command surfaces ``help``'s
301
+ :class:`ClientValidationError` with the remote response as its cause.
302
+ """
303
+ try:
304
+ return await self._execute(
305
+ command,
306
+ params,
307
+ use_cmd_endpoint=use_cmd_endpoint,
308
+ auto_poll=auto_poll,
309
+ timeout=timeout,
310
+ poll_interval=poll_interval,
311
+ status_hook=status_hook,
312
+ )
313
+ except ClientRemoteResponseError as exc:
314
+ if exc.details.get("code") != -32601:
315
+ raise
316
+ self.clear_command_schema_cache()
317
+ try:
318
+ await self.get_command_schema(command, refresh=True)
319
+ except ClientValidationError as schema_error:
320
+ raise schema_error from exc
321
+ raise
322
+
243
323
  @overload
244
324
  async def call_validated(
245
325
  self,
@@ -304,7 +384,7 @@ class CodeAnalysisAsyncClient:
304
384
  merged = dict(params or {})
305
385
  prepared = prepare_params_for_schema(merged, schema)
306
386
  validate_params_against_schema(prepared, schema, command_name=command)
307
- return await self._execute(
387
+ return await self._execute_validated(
308
388
  command,
309
389
  prepared,
310
390
  use_cmd_endpoint=use_cmd_endpoint,
@@ -394,7 +474,7 @@ class CodeAnalysisAsyncClient:
394
474
  merged = dict(params or {})
395
475
  prepared = prepare_params_for_schema(merged, schema)
396
476
  validate_params_against_schema(prepared, schema, command_name=command)
397
- return await self._execute(
477
+ return await self._execute_validated(
398
478
  command,
399
479
  prepared,
400
480
  use_cmd_endpoint=use_cmd_endpoint,
@@ -15,6 +15,8 @@ from typing import Any, Dict, Optional
15
15
  class ClientValidationError(ValueError):
16
16
  """Parameters do not match the command JSON schema (from server ``help``)."""
17
17
 
18
+ code = "VALIDATION_ERROR"
19
+
18
20
  def __init__(
19
21
  self,
20
22
  message: str,
@@ -123,11 +123,17 @@ class FileSessionClient:
123
123
  comment: str,
124
124
  *,
125
125
  role_ids: Optional[List[str]] = None,
126
+ session_id: Optional[str] = None,
127
+ is_foreign: Optional[bool] = None,
126
128
  ) -> str:
127
- """Create a client session (``session_create``) and return ``session_id``."""
129
+ """Create a client session with an optional ID and ownership marker."""
128
130
  params: Dict[str, Any] = {"comment": comment}
129
131
  if role_ids is not None:
130
132
  params["role_ids"] = role_ids
133
+ if session_id is not None:
134
+ params["session_id"] = session_id
135
+ if is_foreign is not None:
136
+ params["is_foreign"] = is_foreign
131
137
  payload = _unwrap(await self._client.call_validated("session_create", params))
132
138
  sid = str(payload.get("session_id") or "").strip()
133
139
  if not sid:
@@ -177,6 +183,17 @@ class FileSessionClient:
177
183
  )
178
184
  )
179
185
 
186
+ async def delete_foreign_session(
187
+ self, session_id: str, *, force: bool = False
188
+ ) -> Dict[str, Any]:
189
+ """Explicitly delete a caller-ID session (``session_delete_foreign``)."""
190
+ params: Dict[str, Any] = {"session_id": _require_non_empty(session_id, field="session_id")}
191
+ if force:
192
+ params["force"] = True
193
+ return _unwrap(
194
+ await self._client.call_validated("session_delete_foreign", params)
195
+ )
196
+
180
197
  async def view_session(self, session_id: str) -> Dict[str, Any]:
181
198
  """Aggregated session view (``session_view``).
182
199
 
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.6.178
3
+ Version: 1.6.187
4
4
  Summary: Async JSON-RPC client for the code-analysis MCP server (mcp-proxy-adapter JsonRpcClient)
5
5
  Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
- Requires-Dist: mcp-proxy-adapter>=8.10.59
8
+ Requires-Dist: mcp-proxy-adapter>=8.10.69
9
9
  Provides-Extra: dev
10
10
  Requires-Dist: pytest>=8.0; extra == "dev"
11
11
  Requires-Dist: pytest-asyncio>=0.25.0; extra == "dev"
@@ -1,6 +1,7 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  code_analysis_client/__init__.py
4
+ code_analysis_client/check_chain.py
4
5
  code_analysis_client/client.py
5
6
  code_analysis_client/commands_proxy.py
6
7
  code_analysis_client/config.py
@@ -1,4 +1,4 @@
1
- mcp-proxy-adapter>=8.10.59
1
+ mcp-proxy-adapter>=8.10.69
2
2
 
3
3
  [dev]
4
4
  pytest>=8.0
@@ -12,7 +12,7 @@ authors = [
12
12
  { name = "Vasiliy Zdanovskiy", email = "vasilyvz@gmail.com" },
13
13
  ]
14
14
  dependencies = [
15
- "mcp-proxy-adapter>=8.10.59",
15
+ "mcp-proxy-adapter>=8.10.69",
16
16
  ]
17
17
 
18
18
  [project.optional-dependencies]