feat!: full-access Gitea MCP server (1.0.0): full API coverage, stdio + streamable HTTP, admin and per-user OAuth #71
No files matched your search
@@ -9,6 +9,7 @@ token to the :class:`~aegis_gitea_mcp.identity.Identity` a tool call runs as.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
@@ -103,6 +104,7 @@ class AdminIdentityResolver:
|
||||
self._executor = executor
|
||||
self._login: str | None = None
|
||||
self._retry_at = 0.0
|
||||
self._lookup_lock = asyncio.Lock()
|
||||
|
||||
async def resolve(self, access: AccessToken | None) -> Identity:
|
||||
"""Return the admin identity, or raise for an access token of another kind."""
|
||||
@@ -112,6 +114,9 @@ class AdminIdentityResolver:
|
||||
if secret is None:
|
||||
raise ToolError("admin-token mode needs GITEA_TOKEN, which is not configured")
|
||||
token = secret.get_secret_value()
|
||||
if self._login is None and time.monotonic() >= self._retry_at:
|
||||
# One lookup for concurrent first calls: later callers wait, then see the result.
|
||||
async with self._lookup_lock:
|
||||
if self._login is None and time.monotonic() >= self._retry_at:
|
||||
self._login = await self._lookup_login(token)
|
||||
if self._login is None:
|
||||
|
||||
+27
-2
@@ -17,8 +17,13 @@ from aegis_gitea_mcp.auth import (
|
||||
StaticBearerVerifier,
|
||||
)
|
||||
from aegis_gitea_mcp.errors import ToolError
|
||||
from aegis_gitea_mcp.gitea import GiteaExecutor
|
||||
from tests.test_server_helpers import API, BEARER_TOKEN, GITEA_TOKEN, server_settings
|
||||
from aegis_gitea_mcp.gitea import GiteaExecutor, ToolOutcome
|
||||
from tests.test_server_helpers import (
|
||||
API,
|
||||
BEARER_TOKEN,
|
||||
GITEA_TOKEN,
|
||||
server_settings,
|
||||
)
|
||||
|
||||
# Static bearer verifier
|
||||
|
||||
@@ -171,3 +176,23 @@ async def test_missing_gitea_token_is_refused() -> None:
|
||||
|
||||
with pytest.raises(ToolError, match="GITEA_TOKEN"):
|
||||
await resolver.resolve(None)
|
||||
|
||||
|
||||
async def test_concurrent_first_calls_send_one_login_lookup() -> None:
|
||||
import asyncio
|
||||
|
||||
lookups: list[str] = []
|
||||
|
||||
class SlowExecutor(GiteaExecutor):
|
||||
async def request(self, method: str, path: str, *args: Any, **kwargs: Any) -> ToolOutcome:
|
||||
lookups.append(path)
|
||||
await asyncio.sleep(0.01) # a real network round trip yields to other calls
|
||||
return ToolOutcome(False, {"data": {"login": "admin"}}, 200)
|
||||
|
||||
settings = server_settings()
|
||||
resolver = AdminIdentityResolver(settings, SlowExecutor(settings))
|
||||
|
||||
identities = await asyncio.gather(*(resolver.resolve(None) for _ in range(10)))
|
||||
|
||||
assert {identity.login for identity in identities} == {"admin"}
|
||||
assert lookups == ["/user"]
|
||||
Reference in new issue
Block a user