feat: basics #1

Merged
albnnc merged 8 commits from itchy-gayal into main 2026-07-06 07:55:40 +00:00
18 changed files with 69 additions and 334 deletions
Showing only changes of commit 40dea9a2c2 - Show all commits
+15 -258
View File
@@ -1,287 +1,44 @@
# OWA MCP Server
MCP (Model Context Protocol) server for any Microsoft Exchange / OWA (Outlook Web Access) deployment. Gives LLM agents access to email, calendar, directory search, folders, availability, and meeting analytics via 30 tools.
Works with any on-premise or hosted Exchange server that exposes OWA.
## Quick Start
```bash
# Install
pip install -e .
# Add to your MCP client config with --owa-url
# Then use the login tool to paste your session cookies
```
## Install
Add to your MCP client config. Replace `https://owa.example.com` with your OWA URL.
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"exchange": {
"command": "uvx",
"args": [
"exchange-mcp-server",
"--owa-url", "https://owa.example.com"
]
}
}
}
```
**Cursor** (`.cursor/mcp.json`):
```json
{
"mcpServers": {
"exchange": {
"command": "uvx",
"args": [
"exchange-mcp-server",
"--owa-url", "https://owa.example.com"
]
}
}
}
```
**Claude Code** (`.mcp.json`):
```json
{
"mcpServers": {
"exchange": {
"command": "uvx",
"args": [
"exchange-mcp-server",
"--owa-url", "https://owa.example.com"
]
}
}
}
```
MCP server for any Microsoft Exchange / OWA deployment. 30 tools for email,
calendar, directory search, folder management, availability, and analytics.
## CLI Arguments
| Argument | Default | Required | Description |
|---|---|---|---|
| `--owa-url` | — | Yes | Base URL of the OWA instance (e.g. `https://owa.example.com`) |
| `--owa-url` | — | Yes | Base URL of the OWA instance |
| `--cookie-file` | `session-cookies.txt` | No | Path to session cookies file |
| `--transport` | `stdio` | — | `stdio`, `sse`, or `streamable-http` |
| `--token-hash` | — | For HTTP | SHA-256 hash of the bearer token |
| `--token-hash` | — | HTTP only | SHA-256 hash of the bearer token |
| `--port` | `8000` | — | HTTP/SSE listener port |
| `--host` | `127.0.0.1` | — | HTTP/SSE listener host |
| `--enabled-tools` | — | No | Comma-separated whitelist of tool names |
| `--disabled-tools` | — | No | Comma-separated blacklist of tool names |
### Tool filtering examples
```bash
# Hide only the most dangerous tools
exchange-mcp-server --owa-url https://owa.example.com \
--disabled-tools "send_email,delete_email,cancel_meeting"
# Allow only email and calendar tools
exchange-mcp-server --owa-url https://owa.example.com \
--enabled-tools "get_emails,get_email,send_email,get_calendar_events,create_meeting"
# Whitelist a set, then remove one from it
exchange-mcp-server --owa-url https://owa.example.com \
--enabled-tools "get_emails,get_email,send_email,delete_email" \
--disabled-tools "delete_email"
# → only get_emails, get_email, send_email remain
```
## HTTP Transport
Run the server over HTTP (streamable-http or SSE) instead of stdio. This is
useful when the MCP client cannot spawn a local process, or when you want to
expose the server on a network.
### Installation
```bash
pip install -e ".[http]" # adds uvicorn + sse-starlette
```
### Start the server
```bash
# Compute the token hash once (store only the hash, never the plaintext token)
TOKEN_HASH=$(echo -n 'my-secret-token' | sha256sum | cut -d' ' -f1)
# stdio (default, for local MCP clients)
exchange-mcp-server --owa-url https://owa.example.com
# streamable-http (recommended for HTTP)
exchange-mcp-server \
--owa-url https://owa.example.com \
--transport streamable-http \
--token-hash "$TOKEN_HASH" \
--port 8000 \
--host 127.0.0.1
# SSE
exchange-mcp-server \
--owa-url https://owa.example.com \
--transport sse \
--token-hash "$TOKEN_HASH" \
--port 8000
```
### CLI arguments
| Argument | Default | Required | Description |
|---|---|---|---|
| `--owa-url` | — | Yes | Base URL of the OWA instance (e.g. `https://owa.example.com`) |
| `--cookie-file` | `session-cookies.txt` | No | Path to session cookies file |
| `--transport` | `stdio` | — | `stdio`, `sse`, or `streamable-http` |
| `--token-hash` | — | For HTTP | SHA-256 hash of the bearer token |
| `--port` | `8000` | — | HTTP/SSE listener port |
| `--host` | `127.0.0.1` | — | HTTP/SSE listener host |
| `--enabled-tools` | — | No | Comma-separated whitelist of tool names |
| `--disabled-tools` | — | No | Comma-separated blacklist of tool names |
### Client configuration (HTTP)
```json
{
"mcpServers": {
"exchange": {
"url": "http://127.0.0.1:8000/mcp",
"headers": {
"Authorization": "Bearer my-secret-token"
}
}
}
}
```
### Authentication
The server stores only the SHA-256 hash of your token — the plaintext token
never touches the filesystem. Every HTTP request must include:
```
Authorization: Bearer <plaintext-token>
```
The server hashes the incoming token and compares it with the stored hash.
Pass the hash via `--token-hash`:
```bash
echo -n 'my-secret-token' | sha256sum
# → 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
```
### Security notes
- Always use a strong random token.
- Bind to `127.0.0.1` unless you have a reverse proxy / firewall.
- For `streamable-http` the client must send `Accept: text/event-stream`.
- The `--token-hash` is required whenever `--transport` is not `stdio`.
## Login
The `login` MCP tool accepts session cookies that you copy from your browser.
1. Log into OWA in your browser.
2. Open DevTools (F12) → Application → Cookies → select your OWA domain.
3. Copy all cookies as `name=value` pairs (one per line).
4. Call the `login` tool with the cookies string.
Example cookies string:
```
X-OWA-CANARY=abc123
CookieAuth1=def456
...
```
```
login(cookies="X-OWA-CANARY=abc123\nCookieAuth1=def456\n...")
```
On success, cookies are saved to `session-cookies.txt` and all tools become usable. When the session expires, repeat the steps.
## Tools (30)
### Email (10)
| Tool | Description |
|---|---|
| `get_emails` | List emails from a folder with filtering |
| `get_email` | Get full email content by ID |
| `send_email` | Send a new email |
| `reply_email` | Reply to an email |
| `forward_email` | Forward an email |
| `delete_email` | Delete an email |
| `move_email` | Move email to another folder |
| `mark_email_read` | Mark email as read/unread |
| `download_attachments` | Download file attachments from an email |
| `get_email_links` | Extract hyperlinks from an email body |
`get_emails`, `get_email`, `send_email`, `reply_email`, `forward_email`,
`delete_email`, `move_email`, `mark_email_read`, `download_attachments`,
`get_email_links`, `search_emails`
### Calendar (7)
| Tool | Description |
|---|---|
| `get_calendar_events` | Get events in a date range (supports recurring expansion) |
| `create_meeting` | Create a meeting with attendees |
| `update_meeting` | Update an existing meeting |
| `cancel_meeting` | Cancel a meeting and notify attendees |
| `respond_to_meeting` | Accept, decline, or tentatively accept |
| `download_event_attachments` | Download file attachments from a calendar event |
| `get_event_links` | Extract hyperlinks from the event description |
`get_calendar_events`, `create_meeting`, `update_meeting`, `cancel_meeting`,
`respond_to_meeting`, `download_event_attachments`, `get_event_links`
### Directory (1)
| Tool | Description |
|---|---|
| `find_person` | Search people in Active Directory |
`find_person`
### Folders (7)
| Tool | Description |
|---|---|
| `get_folders` | List mail folders with unread counts |
| `create_folder` | Create a new mail folder |
| `rename_folder` | Rename an existing folder |
| `empty_folder` | Empty all items from a folder |
| `delete_folder` | Delete a mail folder |
| `move_folder` | Move a folder to a different parent |
| `check_session` | Check if the OWA session is authenticated |
`get_folders`, `create_folder`, `rename_folder`, `empty_folder`, `delete_folder`,
`move_folder`, `check_session`
### Availability (2)
| Tool | Description |
|---|---|
| `find_free_time` | Find free slots in your calendar |
| `find_meeting_time` | Find common free slots for multiple people |
`find_free_time`, `find_meeting_time`
### Analytics (2)
| Tool | Description |
|---|---|
| `get_meeting_stats` | Meeting count statistics for multiple people |
| `get_meeting_contacts` | Connection matrix — who you meet with most |
`get_meeting_stats`, `get_meeting_contacts`
### Auth (1)
| Tool | Description |
|---|---|
| `login` | Authenticate to OWA using browser session cookies |
## Files
```
exchange_mcp/
server.py # FastMCP server entry point
owa_client.py # OWA HTTP client
tools/
email.py # Email tools
calendar.py # Calendar tools
people.py # Directory search
folders.py # Folder management & session check
availability.py # Free time / meeting time
analytics.py # Meeting stats & contacts
auth.py # Login tool
pyproject.toml # Package config
```
`login`
-1
View File
@@ -1 +0,0 @@
"""Exchange MCP Server - access OWA email, calendar, directory via MCP."""
-1
View File
@@ -1 +0,0 @@
"""MCP tool modules for Exchange."""
+1
View File
@@ -0,0 +1 @@
"""OWA MCP Server — access OWA email, calendar, directory via MCP."""
@@ -9,7 +9,6 @@ mirroring what the OWA web frontend sends.
"""
import json
import os
import time
from urllib.parse import quote, urlparse
from pathlib import Path
@@ -55,7 +54,6 @@ class OWAClient:
):
self.cookie_file = Path(
cookie_file
or os.environ.get("EXCHANGE_COOKIE_FILE", "")
or str(Path(__file__).parent.parent / "session-cookies.txt")
)
self.owa_url = (owa_url or "").rstrip("/")
+13 -33
View File
@@ -1,4 +1,4 @@
"""Exchange MCP Server.
"""OWA MCP Server.
Exposes OWA email, calendar, directory, and availability tools via MCP.
Uses FastMCP with a lifespan context manager to share a single OWAClient
@@ -13,19 +13,12 @@ CLI arguments:
--host HTTP host (default 127.0.0.1)
--enabled-tools Comma-separated whitelist of tool names
--disabled-tools Comma-separated blacklist of tool names
Tool filtering (env fallback):
EXCHANGE_ENABLED_TOOLS comma-separated whitelist (used only when
--enabled-tools is not provided).
EXCHANGE_DISABLED_TOOLS comma-separated blacklist (used only when
--disabled-tools is not provided).
"""
from __future__ import annotations
import argparse
import logging
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass
@@ -34,8 +27,8 @@ from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings
from mcp.server.fastmcp import FastMCP
from exchange_mcp.owa_client import OWAClient
from exchange_mcp.server_lifecycle import (
from owa_mcp.owa_client import OWAClient
from owa_mcp.server_lifecycle import (
HashTokenVerifier,
SessionKeepalive,
apply_tool_filters,
@@ -55,7 +48,7 @@ class AppContext:
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
"""Initialise the OWA client on first connection, then keep the session alive.
A :class:`~exchange_mcp.server_lifecycle.SessionKeepalive` task starts
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.
"""
@@ -80,13 +73,13 @@ _lifespan_cookie_file: str | None = None
# ── Module-level MCP server (imported by all tool modules) ────────────────
mcp: FastMCP = FastMCP("exchange", lifespan=app_lifespan)
mcp: FastMCP = FastMCP("owa", lifespan=app_lifespan)
def _parse_args(argv=None) -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Exchange MCP Server — OWA/Exchange integration via MCP",
description="OWA MCP Server — OWA/Exchange integration via MCP",
)
parser.add_argument(
"--transport",
@@ -184,38 +177,25 @@ def main(argv=None) -> None:
raise SystemExit(1)
# ── Register tools (imports populate module-level `mcp`) ───────────────
import exchange_mcp.tools.email # noqa: E402, F401
import exchange_mcp.tools.calendar # noqa: E402, F401
import exchange_mcp.tools.people # noqa: E402, F401
import exchange_mcp.tools.folders # noqa: E402, F401
import exchange_mcp.tools.availability # noqa: E402, F401
import exchange_mcp.tools.analytics # noqa: E402, F401
import exchange_mcp.tools.auth # noqa: E402, F401
import owa_mcp.tools.email # noqa: E402, F401
import owa_mcp.tools.calendar # noqa: E402, F401
import owa_mcp.tools.people # noqa: E402, F401
import owa_mcp.tools.folders # noqa: E402, F401
import owa_mcp.tools.availability # noqa: E402, F401
import owa_mcp.tools.analytics # noqa: E402, F401
import owa_mcp.tools.auth # noqa: E402, F401
# ── Auth for HTTP transports ───────────────────────────────────────────
_configure_auth(mcp, args.token_hash)
# ── Tool filtering ─────────────────────────────────────────────────────
# CLI args take precedence over env vars.
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()]
elif os.environ.get("EXCHANGE_ENABLED_TOOLS", "").strip():
enabled = [
t.strip()
for t in os.environ["EXCHANGE_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()]
elif os.environ.get("EXCHANGE_DISABLED_TOOLS", "").strip():
disabled = [
t.strip()
for t in os.environ["EXCHANGE_DISABLED_TOOLS"].split(",")
if t.strip()
]
if enabled or disabled:
apply_tool_filters(mcp, enabled=enabled, disabled=disabled)
@@ -1,4 +1,4 @@
"""Shared lifecycle utilities for the Exchange MCP server.
"""Shared lifecycle utilities for the OWA MCP server.
Centralises three concerns so that tool modules do not duplicate code:
@@ -17,7 +17,7 @@ import asyncio
import logging
from typing import TYPE_CHECKING
from exchange_mcp.owa_client import OWAClient, SessionExpiredError
from owa_mcp.owa_client import OWAClient, SessionExpiredError
from mcp.server.auth.provider import AccessToken, TokenVerifier
if TYPE_CHECKING:
+1
View File
@@ -0,0 +1 @@
"""MCP tool modules for OWA."""
@@ -1,4 +1,4 @@
"""Analytics tools for the Exchange MCP server.
"""Analytics tools for the OWA MCP server.
Provides meeting statistics and connection matrix analysis
using GetUserAvailability and GetItem APIs.
@@ -10,9 +10,9 @@ from datetime import datetime, timedelta, date
from mcp.server.fastmcp import Context
from exchange_mcp.server import mcp, AppContext
from exchange_mcp.owa_client import OWAClient
from exchange_mcp.server_lifecycle import require_client
from owa_mcp.server import mcp, AppContext
from owa_mcp.owa_client import OWAClient
from owa_mcp.server_lifecycle import require_client
def _get_client(ctx: Context) -> OWAClient:
@@ -1,4 +1,4 @@
"""Authentication tool for the Exchange MCP server.
"""Authentication tool for the OWA MCP server.
Provides a `login` tool that accepts session cookies from the user,
saves them to disk, loads them into the OWA client, and verifies the session.
@@ -12,9 +12,9 @@ import json
from mcp.server.fastmcp import Context
from exchange_mcp.server import mcp, AppContext
from exchange_mcp.owa_client import OWAClient, SessionExpiredError
from exchange_mcp.server_lifecycle import require_client
from owa_mcp.server import mcp, AppContext
from owa_mcp.owa_client import OWAClient, SessionExpiredError
from owa_mcp.server_lifecycle import require_client
def _get_app_ctx(ctx: Context) -> AppContext:
@@ -1,4 +1,4 @@
"""Availability / free-time tools for the Exchange MCP server.
"""Availability / free-time tools for the OWA MCP server.
Ports find-free-time.py and find-meeting-time.py logic into MCP tools
using OWAClient.
@@ -9,9 +9,9 @@ from datetime import datetime, timedelta
from mcp.server.fastmcp import Context
from exchange_mcp.server import mcp, AppContext
from exchange_mcp.owa_client import OWAClient
from exchange_mcp.server_lifecycle import require_client
from owa_mcp.server import mcp, AppContext
from owa_mcp.owa_client import OWAClient
from owa_mcp.server_lifecycle import require_client
def _get_client(ctx: Context) -> OWAClient:
@@ -1,4 +1,4 @@
"""Calendar tools for the Exchange MCP server.
"""Calendar tools for the OWA MCP server.
Provides MCP tools for calendar event retrieval, meeting creation,
update, cancellation, and meeting response management via OWA API.
@@ -11,10 +11,10 @@ from datetime import datetime, timedelta
from mcp.server.fastmcp import Context
from exchange_mcp.server import mcp, AppContext
from exchange_mcp.owa_client import OWAClient
from exchange_mcp.server_lifecycle import require_client
from exchange_mcp.utils import html_to_text, parse_iso_datetime, extract_links_from_html
from owa_mcp.server import mcp, AppContext
from owa_mcp.owa_client import OWAClient
from owa_mcp.server_lifecycle import require_client
from owa_mcp.utils import html_to_text, parse_iso_datetime, extract_links_from_html
def _get_client(ctx: Context) -> OWAClient:
@@ -1,4 +1,4 @@
"""Email tools for the Exchange MCP server.
"""Email tools for the OWA MCP server.
Provides tools for reading, sending, replying, forwarding, and managing
emails via the OWA Exchange API.
@@ -8,10 +8,10 @@ import json
from mcp.server.fastmcp import Context
from exchange_mcp.server import mcp, AppContext
from exchange_mcp.owa_client import OWAClient, SessionExpiredError
from exchange_mcp.server_lifecycle import require_client
from exchange_mcp.utils import html_to_text, extract_links_from_html
from owa_mcp.server import mcp, AppContext
from owa_mcp.owa_client import OWAClient, SessionExpiredError
from owa_mcp.server_lifecycle import require_client
from owa_mcp.utils import html_to_text, extract_links_from_html
def _get_client(ctx: Context) -> OWAClient:
@@ -1,4 +1,4 @@
"""Folder tools for the Exchange MCP server.
"""Folder tools for the OWA MCP server.
Ports the get-folders.py logic into an MCP tool using OWAClient.
"""
@@ -7,9 +7,9 @@ import json
from mcp.server.fastmcp import Context
from exchange_mcp.server import mcp, AppContext
from exchange_mcp.owa_client import OWAClient
from exchange_mcp.server_lifecycle import require_client
from owa_mcp.server import mcp, AppContext
from owa_mcp.owa_client import OWAClient
from owa_mcp.server_lifecycle import require_client
_DISTINGUISHED_NAMES = {
@@ -1,4 +1,4 @@
"""People / directory search tools for the Exchange MCP server.
"""People / directory search tools for the OWA MCP server.
Ports the find-person.py logic into an MCP tool using OWAClient.
"""
@@ -7,9 +7,9 @@ import json
from mcp.server.fastmcp import Context
from exchange_mcp.server import mcp, AppContext
from exchange_mcp.owa_client import OWAClient
from exchange_mcp.server_lifecycle import require_client
from owa_mcp.server import mcp, AppContext
from owa_mcp.owa_client import OWAClient
from owa_mcp.server_lifecycle import require_client
def _get_client(ctx: Context) -> OWAClient:
+1 -1
View File
@@ -1,4 +1,4 @@
"""Shared utility functions for the Exchange MCP server.
"""Shared utility functions for the OWA MCP server.
Extracts duplicated helpers from the standalone scripts:
html_to_text, date/time formatting and parsing.
+6 -6
View File
@@ -1,11 +1,11 @@
[project]
name = "exchange-mcp-server"
version = "1.0.1"
description = "MCP server for Exchange / OWA - email, calendar, directory, availability"
name = "owa-mcp-server"
version = "0.1.0"
description = "MCP server for OWA email, calendar, directory, availability"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
keywords = ["mcp", "exchange", "owa", "outlook", "email", "calendar"]
keywords = ["mcp", "owa", "outlook", "exchange", "email", "calendar"]
classifiers = [
"Programming Language :: Python :: 3",
]
@@ -23,7 +23,7 @@ Repository = "https://git.kosyrev.name/zoo/owa-mcp"
http = ["uvicorn>=0.30.0"]
[project.scripts]
exchange-mcp-server = "exchange_mcp.server:main"
owa-mcp-server = "owa_mcp.server:main"
[build-system]
requires = ["setuptools>=68.0"]
@@ -31,4 +31,4 @@ build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["."]
include = ["exchange_mcp*"]
include = ["owa_mcp*"]