w
This commit is contained in:
+140
-141
@@ -29,10 +29,10 @@ from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from owa_mcp.owa_client import OWAClient
|
||||
from owa_mcp.server_lifecycle import (
|
||||
HashTokenVerifier,
|
||||
SessionKeepalive,
|
||||
apply_tool_filters,
|
||||
create_owa_client,
|
||||
HashTokenVerifier,
|
||||
SessionKeepalive,
|
||||
apply_tool_filters,
|
||||
create_owa_client,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -40,32 +40,32 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class AppContext:
|
||||
"""Shared application state available to all tools via lifespan context."""
|
||||
"""Shared application state available to all tools via lifespan context."""
|
||||
|
||||
client: OWAClient | None = None
|
||||
client: OWAClient | None = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
||||
"""Initialise the OWA client on first connection, then keep the session alive.
|
||||
"""Initialise the OWA client on first connection, then keep the session alive.
|
||||
|
||||
A :class:`~owa_mcp.server_lifecycle.SessionKeepalive` task starts
|
||||
immediately (pings inbox, extends the session, persists fresh cookies) and
|
||||
repeats every 5 minutes until the server shuts down.
|
||||
"""
|
||||
client = create_owa_client(_lifespan_owa_url, _lifespan_cookie_file)
|
||||
try:
|
||||
client._ensure_loaded()
|
||||
except Exception:
|
||||
pass # no cookies yet — login tool will handle this
|
||||
A :class:`~owa_mcp.server_lifecycle.SessionKeepalive` task starts
|
||||
immediately (pings inbox, extends the session, persists fresh cookies) and
|
||||
repeats every 5 minutes until the server shuts down.
|
||||
"""
|
||||
client = create_owa_client(_lifespan_owa_url, _lifespan_cookie_file)
|
||||
try:
|
||||
client._ensure_loaded()
|
||||
except Exception:
|
||||
pass # no cookies yet — login tool will handle this
|
||||
|
||||
keepalive = SessionKeepalive(client)
|
||||
await keepalive.start()
|
||||
keepalive = SessionKeepalive(client)
|
||||
await keepalive.start()
|
||||
|
||||
try:
|
||||
yield AppContext(client=client)
|
||||
finally:
|
||||
await keepalive.stop()
|
||||
try:
|
||||
yield AppContext(client=client)
|
||||
finally:
|
||||
await keepalive.stop()
|
||||
|
||||
|
||||
# ── Temporary storage for lifespan args (set by main() before mcp.run()) ──
|
||||
@@ -78,144 +78,143 @@ mcp: FastMCP = FastMCP("owa", lifespan=app_lifespan)
|
||||
|
||||
|
||||
def _parse_args(argv=None) -> argparse.Namespace:
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="OWA MCP Server — OWA/Exchange integration via MCP",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transport",
|
||||
choices=["stdio", "sse", "streamable-http"],
|
||||
default="stdio",
|
||||
help="Transport protocol (default: stdio)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--owa-url",
|
||||
required=True,
|
||||
help="Base URL of the OWA instance (e.g. https://owa.example.com)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cookie-file",
|
||||
default=None,
|
||||
help="Path to session cookies file (default: session-cookies.txt)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--token-hash",
|
||||
default=None,
|
||||
help=(
|
||||
"SHA-256 hash of the bearer token for HTTP transport auth. "
|
||||
"Compute with: echo -n 'my-token' | sha256sum"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Port for HTTP/SSE transport (default: 8000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="127.0.0.1",
|
||||
help="Host for HTTP/SSE transport (default: 127.0.0.1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enabled-tools",
|
||||
default=None,
|
||||
help="Comma-separated whitelist of tool names to expose",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disabled-tools",
|
||||
default=None,
|
||||
help="Comma-separated blacklist of tool names to hide",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="OWA MCP Server — OWA/Exchange integration via MCP",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transport",
|
||||
choices=["stdio", "sse", "streamable-http"],
|
||||
default="stdio",
|
||||
help="Transport protocol (default: stdio)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--owa-url",
|
||||
required=True,
|
||||
help="Base URL of the OWA instance (e.g. https://owa.example.com)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cookie-file",
|
||||
default=None,
|
||||
help="Path to session cookies file (default: session-cookies.txt)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--token-hash",
|
||||
default=None,
|
||||
help=(
|
||||
"SHA-256 hash of the bearer token for HTTP transport auth. Compute with: echo -n 'my-token' | sha256sum"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Port for HTTP/SSE transport (default: 8000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="127.0.0.1",
|
||||
help="Host for HTTP/SSE transport (default: 127.0.0.1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enabled-tools",
|
||||
default=None,
|
||||
help="Comma-separated whitelist of tool names to expose",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disabled-tools",
|
||||
default=None,
|
||||
help="Comma-separated blacklist of tool names to hide",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def _configure_auth(
|
||||
mcp_server: FastMCP,
|
||||
token_hash: str | None,
|
||||
mcp_server: FastMCP,
|
||||
token_hash: str | None,
|
||||
) -> None:
|
||||
"""Apply token-hash auth to *mcp_server* if *token_hash* is provided."""
|
||||
if not token_hash:
|
||||
return
|
||||
"""Apply token-hash auth to *mcp_server* if *token_hash* is provided."""
|
||||
if not token_hash:
|
||||
return
|
||||
|
||||
token_verifier: TokenVerifier = HashTokenVerifier(token_hash)
|
||||
mcp_server.settings.auth = AuthSettings( # type: ignore[misc]
|
||||
issuer_url="http://localhost", # type: ignore[arg-type]
|
||||
resource_server_url="http://localhost", # type: ignore[arg-type]
|
||||
)
|
||||
mcp_server._token_verifier = token_verifier # noqa: SLF001
|
||||
token_verifier: TokenVerifier = HashTokenVerifier(token_hash)
|
||||
mcp_server.settings.auth = AuthSettings( # type: ignore[misc]
|
||||
issuer_url="http://localhost", # type: ignore[arg-type]
|
||||
resource_server_url="http://localhost", # type: ignore[arg-type]
|
||||
)
|
||||
mcp_server._token_verifier = token_verifier # noqa: SLF001
|
||||
|
||||
|
||||
def main(argv=None) -> None:
|
||||
"""Entry point: parse CLI args, configure server, run transport."""
|
||||
global _lifespan_owa_url, _lifespan_cookie_file # noqa: PLW0603
|
||||
"""Entry point: parse CLI args, configure server, run transport."""
|
||||
global _lifespan_owa_url, _lifespan_cookie_file # noqa: PLW0603
|
||||
|
||||
args = _parse_args(argv)
|
||||
args = _parse_args(argv)
|
||||
|
||||
# ── Validate --owa-url ─────────────────────────────────────────────────
|
||||
owa_url = args.owa_url.strip().rstrip("/")
|
||||
if not owa_url:
|
||||
print(
|
||||
"ERROR: --owa-url must not be empty.",
|
||||
file=__import__("sys").stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
# ── Validate --owa-url ─────────────────────────────────────────────────
|
||||
owa_url = args.owa_url.strip().rstrip("/")
|
||||
if not owa_url:
|
||||
print(
|
||||
"ERROR: --owa-url must not be empty.",
|
||||
file=__import__("sys").stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
# ── Stash lifespan args (consumed by app_lifespan) ─────────────────────
|
||||
_lifespan_owa_url = owa_url
|
||||
_lifespan_cookie_file = args.cookie_file
|
||||
# ── Stash lifespan args (consumed by app_lifespan) ─────────────────────
|
||||
_lifespan_owa_url = owa_url
|
||||
_lifespan_cookie_file = args.cookie_file
|
||||
|
||||
# ── Validate --token-hash for non-stdio transports ─────────────────────
|
||||
if args.transport != "stdio" and not args.token_hash:
|
||||
print(
|
||||
"ERROR: --token-hash is required when using HTTP transport.",
|
||||
file=__import__("sys").stderr,
|
||||
)
|
||||
print(
|
||||
" Compute hash: echo -n 'my-token' | sha256sum",
|
||||
file=__import__("sys").stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
# ── Validate --token-hash for non-stdio transports ─────────────────────
|
||||
if args.transport != "stdio" and not args.token_hash:
|
||||
print(
|
||||
"ERROR: --token-hash is required when using HTTP transport.",
|
||||
file=__import__("sys").stderr,
|
||||
)
|
||||
print(
|
||||
" Compute hash: echo -n 'my-token' | sha256sum",
|
||||
file=__import__("sys").stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
# ── Register tools (imports populate module-level `mcp`) ───────────────
|
||||
import owa_mcp.tools.analytics # noqa: E402, F401
|
||||
import owa_mcp.tools.auth # noqa: E402, F401
|
||||
import owa_mcp.tools.availability # noqa: E402, F401
|
||||
import owa_mcp.tools.calendar # noqa: E402, F401
|
||||
import owa_mcp.tools.email # noqa: E402, F401
|
||||
import owa_mcp.tools.folders # noqa: E402, F401
|
||||
import owa_mcp.tools.people # noqa: E402, F401
|
||||
# ── Register tools (imports populate module-level `mcp`) ───────────────
|
||||
import owa_mcp.tools.analytics # noqa: E402, F401
|
||||
import owa_mcp.tools.auth # noqa: E402, F401
|
||||
import owa_mcp.tools.availability # noqa: E402, F401
|
||||
import owa_mcp.tools.calendar # noqa: E402, F401
|
||||
import owa_mcp.tools.email # noqa: E402, F401
|
||||
import owa_mcp.tools.folders # noqa: E402, F401
|
||||
import owa_mcp.tools.people # noqa: E402, F401
|
||||
|
||||
# ── Auth for HTTP transports ───────────────────────────────────────────
|
||||
_configure_auth(mcp, args.token_hash)
|
||||
# ── Auth for HTTP transports ───────────────────────────────────────────
|
||||
_configure_auth(mcp, args.token_hash)
|
||||
|
||||
# ── Tool filtering ─────────────────────────────────────────────────────
|
||||
enabled: list[str] | None = None
|
||||
if args.enabled_tools is not None:
|
||||
enabled = [t.strip() for t in args.enabled_tools.split(",") if t.strip()]
|
||||
# ── Tool filtering ─────────────────────────────────────────────────────
|
||||
enabled: list[str] | None = None
|
||||
if args.enabled_tools is not None:
|
||||
enabled = [t.strip() for t in args.enabled_tools.split(",") if t.strip()]
|
||||
|
||||
disabled: list[str] | None = None
|
||||
if args.disabled_tools is not None:
|
||||
disabled = [t.strip() for t in args.disabled_tools.split(",") if t.strip()]
|
||||
disabled: list[str] | None = None
|
||||
if args.disabled_tools is not None:
|
||||
disabled = [t.strip() for t in args.disabled_tools.split(",") if t.strip()]
|
||||
|
||||
if enabled or disabled:
|
||||
apply_tool_filters(mcp, enabled=enabled, disabled=disabled)
|
||||
if enabled or disabled:
|
||||
apply_tool_filters(mcp, enabled=enabled, disabled=disabled)
|
||||
|
||||
# ── Host / port (already passed to FastMCP at module level; update if
|
||||
# different from defaults — useful when server is re-invoked with
|
||||
# different args in tests) ─────────────────────────────────────────────
|
||||
mcp.settings.host = args.host
|
||||
mcp.settings.port = args.port
|
||||
# ── Host / port (already passed to FastMCP at module level; update if
|
||||
# different from defaults — useful when server is re-invoked with
|
||||
# different args in tests) ─────────────────────────────────────────────
|
||||
mcp.settings.host = args.host
|
||||
mcp.settings.port = args.port
|
||||
|
||||
# ── Run transport ─────────────────────────────────────────────────────
|
||||
match args.transport:
|
||||
case "stdio":
|
||||
mcp.run()
|
||||
case "sse":
|
||||
mcp.run(transport="sse")
|
||||
case "streamable-http":
|
||||
mcp.run(transport="streamable-http")
|
||||
# ── Run transport ─────────────────────────────────────────────────────
|
||||
match args.transport:
|
||||
case "stdio":
|
||||
mcp.run()
|
||||
case "sse":
|
||||
mcp.run(transport="sse")
|
||||
case "streamable-http":
|
||||
mcp.run(transport="streamable-http")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user