w
This commit is contained in:
+39
-23
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user