fix(mcp): harden project context issue provider and prove shared audit path

- Share cursor decoding in foundation.decode_offset_cursor so both tools
  reject an invalid cursor identically, before any upstream call.
- Add regression coverage that a malformed repo slug (owner/repo/extra,
  missing owner, empty segment) is refused before any network call.
- Add evidence that BOTH project context tools inherit the shared MCP
  client audit record and untrusted-content fence, instead of each tool
  shipping its own. No audit subsystem is duplicated.
This commit is contained in:
thanhnv
2026-09-04 10:46:17 +09:00
parent 218ee29893
commit fecbe7cb25
4 changed files with 108 additions and 14 deletions
+17
View File
@@ -62,6 +62,23 @@ class ProviderError(RuntimeError):
self.retryable = retryable
def decode_offset_cursor(cursor: str | None) -> int:
"""Shared opaque-cursor decoding for every paginated provider.
Rejected before any backend call so an invalid cursor never costs an
upstream request.
"""
if cursor is None:
return 0
try:
offset = int(cursor)
except ValueError as exc:
raise ProviderError("INVALID_INPUT", "cursor is not valid.", retryable=False) from exc
if offset < 0:
raise ProviderError("INVALID_INPUT", "cursor is not valid.", retryable=False)
return offset
ToolHandler = Callable[[ContractModel, Any], dict[str, Any]]
+2 -13
View File
@@ -16,7 +16,7 @@ from typing import Any, Protocol
import requests
from ..foundation import IdentityContext, ProviderError
from ..foundation import IdentityContext, ProviderError, decode_offset_cursor
# ---- tunables (documented, not hardcoded secrets) -------------------------
_REQUEST_TIMEOUT_SECONDS = 10
@@ -203,7 +203,7 @@ class GiteaIssueProvider:
"issue_key must be a positive work item number.",
retryable=False,
)
offset = self._decode_cursor(cursor)
offset = decode_offset_cursor(cursor)
payload = self._fetch_issue(issue_key)
@@ -256,17 +256,6 @@ class GiteaIssueProvider:
}
# ---- internals ---------------------------------------------------
def _decode_cursor(self, cursor: str | None) -> int:
if cursor is None:
return 0
try:
offset = int(cursor)
except ValueError as exc:
raise ProviderError("INVALID_INPUT", "cursor is not valid.", retryable=False) from exc
if offset < 0:
raise ProviderError("INVALID_INPUT", "cursor is not valid.", retryable=False)
return offset
def _build_description(self, body: str, detail: str) -> str:
text = body.strip()
if detail == "summary":