This commit is contained in:
2026-07-06 02:05:16 +03:00
parent 40dea9a2c2
commit 4207b38b16
14 changed files with 823 additions and 607 deletions
+39 -23
View File
@@ -17,9 +17,10 @@ import asyncio
import logging
from typing import TYPE_CHECKING
from owa_mcp.owa_client import OWAClient, SessionExpiredError
from mcp.server.auth.provider import AccessToken, TokenVerifier
from owa_mcp.owa_client import OWAClient, SessionExpiredError
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP
@@ -28,6 +29,7 @@ logger = logging.getLogger(__name__)
# ── Client factory ───────────────────────────────────────────────────────
def create_owa_client(owa_url: str, cookie_file: str | None = None) -> OWAClient:
"""Create and return a fully configured :class:`OWAClient`.
@@ -50,6 +52,7 @@ def create_owa_client(owa_url: str, cookie_file: str | None = None) -> OWAClient
# ── Token verifier ────────────────────────────────────────────────────────
class HashTokenVerifier(TokenVerifier):
"""Bearer-token verifier backed by a SHA-256 hash.
@@ -59,11 +62,12 @@ class HashTokenVerifier(TokenVerifier):
"""
def __init__(self, token_hash: str) -> None:
import hashlib
self._expected_hash = token_hash.strip().lower()
async def verify_token(self, token: str) -> AccessToken | None:
import hashlib
if hashlib.sha256(token.encode()).hexdigest() == self._expected_hash:
return AccessToken(
token=token,
@@ -75,6 +79,7 @@ class HashTokenVerifier(TokenVerifier):
# ── Lifespan helper ──────────────────────────────────────────────────────
class _LazyClient:
"""Thin wrapper that resolves the OWAClient on first access.
@@ -103,8 +108,7 @@ def require_client(client: OWAClient | None) -> OWAClient:
"""
if client is None:
raise RuntimeError(
"OWA client is not initialised. "
"Pass --owa-url when starting the server."
"OWA client is not initialised. Pass --owa-url when starting the server."
)
return client
@@ -127,7 +131,9 @@ class SessionKeepalive:
picked up without restarting the server.
"""
def __init__(self, client: OWAClient, interval: int = _DEFAULT_KEEPALIVE_INTERVAL) -> None:
def __init__(
self, client: OWAClient, interval: int = _DEFAULT_KEEPALIVE_INTERVAL
) -> None:
self._client = client
self._interval = interval
self._task: asyncio.Task | None = None
@@ -168,37 +174,47 @@ class SessionKeepalive:
case the user re-authenticated while the server was running.
"""
try:
self._client.request("GetFolder", {
"__type": "GetFolderJsonRequest:#Exchange",
"Header": {
"__type": "JsonRequestHeaders:#Exchange",
"RequestServerVersion": "Exchange2013",
},
"Body": {
"__type": "GetFolderRequest:#Exchange",
"FolderShape": {
"__type": "FolderResponseShape:#Exchange",
"BaseShape": "IdOnly",
self._client.request(
"GetFolder",
{
"__type": "GetFolderJsonRequest:#Exchange",
"Header": {
"__type": "JsonRequestHeaders:#Exchange",
"RequestServerVersion": "Exchange2013",
},
"Body": {
"__type": "GetFolderRequest:#Exchange",
"FolderShape": {
"__type": "FolderResponseShape:#Exchange",
"BaseShape": "IdOnly",
},
"FolderIds": [
{
"__type": "DistinguishedFolderId:#Exchange",
"Id": "inbox",
}
],
},
"FolderIds": [{
"__type": "DistinguishedFolderId:#Exchange",
"Id": "inbox",
}],
},
})
)
logger.debug("Session keepalive ping succeeded")
except SessionExpiredError:
logger.warning("Session expired during keepalive — reloading cookies from disk")
logger.warning(
"Session expired during keepalive — reloading cookies from disk"
)
try:
self._client._ensure_loaded()
except Exception as exc:
logger.warning("Failed to reload cookies after keepalive failure: %s", exc)
logger.warning(
"Failed to reload cookies after keepalive failure: %s", exc
)
except Exception as exc:
logger.debug("Keepalive ping failed (non-critical): %s", exc)
# ── Tool filtering ───────────────────────────────────────────────────────
def apply_tool_filters(
server: FastMCP,
enabled: list[str] | None = None,