Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 42 additions & 22 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,28 +185,48 @@ Choose the appropriate setup based on your authentication method:

**6. Authentication Flow**

OAuth authentication in this MCP server is handled through **FastMCP's OIDCProxy**, which implements a secure two-layer authentication:

1. **Layer 1 - MCP Client Authentication**:
- MCP client (e.g., Claude Desktop, MCP Inspector) authenticates with the FastMCP server
- FastMCP proxy authenticates the user with Microsoft Entra ID
- Uses PKCE (Proof Key for Code Exchange) for security without client secrets on the client side

2. **Layer 2 - SharePoint API Access**:
- The authenticated user's token is used to access SharePoint APIs on their behalf
- User's delegated permissions are used (AllSites.Read/Write)

**Security Features**:
- PKCE (Proof Key for Code Exchange) ensures tokens cannot be intercepted
- Client secrets are only stored on the server side (`.env` file)
- Token validation trusts Azure AD's OAuth flow (no additional JWT verification required for SharePoint tokens)

**Important Notes**:
- Authentication is performed through the MCP client's OAuth flow with Azure AD
- No manual browser login is required - the MCP client handles the OAuth flow automatically
- Tokens are managed and validated by FastMCP (using custom SharePointTokenVerifier)
- The server uses the `/auth/callback` endpoint (FastMCP standard) for OAuth callbacks
- MCP clients can use dynamic ports (e.g., http://localhost:6274/oauth/callback) as FastMCP accepts wildcard localhost URIs
OAuth authentication in this MCP server supports two token acquisition methods:

**Method 1: FastMCP OAuth Flow (Recommended)**

Handled through **FastMCP's OIDCProxy**, which implements a secure two-layer authentication:

1. **MCP Client Authentication**: MCP client authenticates with FastMCP server, which then authenticates the user with Microsoft Entra ID using PKCE
2. **SharePoint API Access**: The authenticated user's token is used to access SharePoint APIs with delegated permissions (AllSites.Read/Write)

Features:
- No manual browser login required - MCP client handles the OAuth flow automatically
- PKCE ensures tokens cannot be intercepted
- Tokens are managed and validated by FastMCP
- Uses `/auth/callback` endpoint for OAuth callbacks
- Supports dynamic ports (e.g., http://localhost:6274/oauth/callback)

**Method 2: Direct Token in Authorization Header (HTTP Transport Only)**

For advanced scenarios such as testing, custom integrations, or existing token management systems:

**Important**: This method requires `--transport http` mode. It is not available in stdio mode (used by Claude Desktop and similar MCP clients).

- Acquire an access token externally (e.g., via Azure CLI, custom script)
- Pass the token in the `Authorization: Bearer <token>` HTTP header when calling MCP tools via HTTP
- The server uses the provided token directly without performing the OAuth flow
- Primarily intended for testing, debugging, and custom HTTP-based integrations

Example using Azure CLI:
```bash
# Get token for SharePoint
az account get-access-token --resource https://yourtenant.sharepoint.com --query accessToken -o tsv

# Use with curl to test the MCP server directly
curl -X POST http://localhost:8000/mcp \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"method": "sharepoint_docs_search", "params": {"query": "test"}}'
```

**Note**: Standard MCP clients (Claude Desktop, etc.) use stdio transport and cannot use this method. Use Method 1 (FastMCP OAuth Flow) for standard MCP clients.

**Security Consideration**: Ensure your token has the required SharePoint scopes (`https://<tenant>.sharepoint.com/.default`) and manage token validity/refresh yourself.

## Tool Description Customization

Expand Down
56 changes: 38 additions & 18 deletions docs/setup_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,28 +185,48 @@ rm cert/certificate.csr

**6. 認証フロー**

このMCPサーバーのOAuth認証は**FastMCPのOIDCProxy**によって処理され、安全な2層認証を実装します
このMCPサーバーのOAuth認証は2つのトークン取得方法をサポートします:

1. **レイヤー1 - MCPクライアント認証**
- MCPクライアント(Claude Desktop、MCP Inspectorなど)がFastMCPサーバーに接続
- FastMCPプロキシがユーザーをMicrosoft Entra IDで認証
- クライアント側でシークレットが不要なPKCEを使用してセキュリティを確保
**方法1: FastMCP OAuthフロー(推奨)**

2. **レイヤー2 - SharePoint APIアクセス**
- 認証されたユーザーのトークンを使用してSharePoint APIにアクセス
- ユーザーの委任された権限を使用(AllSites.Read/Write)
**FastMCPのOIDCProxy**によって処理され、安全な2層認証を実装します:

**セキュリティ機能**
1. **MCPクライアント認証**: MCPクライアントがFastMCPサーバーに接続し、FastMCPがMicrosoft Entra IDでユーザー認証(PKCEを使用)
2. **SharePoint APIアクセス**: 認証されたユーザーのトークンでSharePoint APIにアクセス(委任された権限: AllSites.Read/Write)

特徴:
- 手動でブラウザログイン不要 - MCPクライアントがOAuthフローを自動処理
- PKCEによりトークンの傍受を防止
- クライアントシークレットはサーバー側でのみ保存
- トークン検証はAzure ADのOAuthフローを信頼

**重要な注意事項**
- 認証はMCPクライアントのOAuthフローを通じて実行されます
- 手動でブラウザログインする必要はありません - MCPクライアントがOAuthフローを自動処理
- トークンはFastMCPによって管理され、安全にキャッシュされます
- サーバーは `/auth/callback` エンドポイント(FastMCP標準)をOAuthコールバックに使用
- MCPクライアントは動的ポート(例: http://localhost:6274/oauth/callback )を使用可能で、FastMCPはワイルドカードlocalhost URIを許可
- トークンはFastMCPによって管理・検証
- `/auth/callback` エンドポイントをOAuthコールバックに使用
- 動的ポート対応(例: http://localhost:6274/oauth/callback)

**方法2: Authorizationヘッダーで直接トークンを渡す(HTTPトランスポート専用)**

テスト、カスタム統合、既存のトークン管理システム向けの高度なシナリオ:

**重要**: この方法は `--transport http` モードが必要です。stdioモード(Claude Desktopなどの標準MCPクライアントで使用)では利用できません。

- 外部でアクセストークンを取得(Azure CLI、カスタムスクリプトなど)
- HTTP経由でMCPツールを呼び出す際に `Authorization: Bearer <token>` HTTPヘッダーでトークンを渡す
- サーバーはOAuthフローを実行せず、提供されたトークンを直接使用
- 主にテスト、デバッグ、カスタムHTTPベース統合向け

Azure CLIを使用した例:
```bash
# SharePoint用のトークンを取得
az account get-access-token --resource https://yourtenant.sharepoint.com --query accessToken -o tsv

# curlでMCPサーバーを直接テスト
curl -X POST http://localhost:8000/mcp \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"method": "sharepoint_docs_search", "params": {"query": "test"}}'
```

**注意**: 標準MCPクライアント(Claude Desktopなど)はstdioトランスポートを使用するため、この方法は使用できません。標準MCPクライアントには方法1(FastMCP OAuthフロー)を使用してください。

**セキュリティ上の注意**: トークンが必要なSharePointスコープ(`https://<tenant>.sharepoint.com/.default`)を持つことを確認し、トークンの有効性と更新を自分で管理してください。

## ツール説明文のカスタマイズ

Expand Down
68 changes: 58 additions & 10 deletions src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Any
from urllib.parse import parse_qs, urlencode, urlparse, urlsplit, urlunsplit

from fastmcp import FastMCP
from fastmcp import Context, FastMCP
from fastmcp.server.auth import AccessToken, TokenVerifier
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.server.dependencies import get_access_token
Expand Down Expand Up @@ -236,11 +236,56 @@ def _get_auth_client() -> SharePointCertificateAuth | None:
)


def _get_sharepoint_client() -> SharePointSearchClient:
def _get_token_from_request(ctx: Context | None = None) -> str | None:
"""Get token from Authorization header or FastMCP context

Tries to get token in the following order:
1. From Authorization header (direct token approach)
2. From FastMCP's OAuth flow (get_access_token)

Args:
ctx: FastMCP context (optional)

Returns:
Token string if available, None otherwise
"""
# Try to get from HTTP header first (direct token)
if ctx:
try:
request = ctx.get_http_request()
except RuntimeError as e:
# Not in HTTP context (e.g., stdio mode) - expected behavior
logging.debug(f"Not in HTTP context, skipping Authorization header: {e}")
except AttributeError as e:
# Unexpected attribute error - may indicate a code bug
logging.warning(f"Unexpected error accessing HTTP request: {e}")
else:
auth_header = request.headers.get("Authorization", "")
if auth_header.lower().startswith("bearer "):
token = auth_header[len("bearer ") :].strip()
if token:
logging.info("Token retrieved from Authorization header")
return token
else:
logging.warning("Empty token in Authorization header")

# Fallback to FastMCP's OAuth flow token
access_token = get_access_token()
if access_token:
logging.info("Token retrieved from FastMCP OAuth context")
return access_token.token

return None


def _get_sharepoint_client(ctx: Context | None = None) -> SharePointSearchClient:
"""SharePointクライアントを取得または初期化

- 証明書モード: シングルトンクライアントを使用
- OAuthモード: リクエストごとに新しいクライアントを作成(トークンはリクエスト依存)

Args:
ctx: FastMCP context for accessing HTTP request (OAuth mode only)
"""
global _sharepoint_client

Expand All @@ -256,16 +301,16 @@ def _get_sharepoint_client() -> SharePointSearchClient:

# OAuthモード: リクエストごとに新しいクライアントを作成
if config.is_oauth_mode:
# FastMCPの認証コンテキストからトークンを取得
access_token = get_access_token()
if not access_token:
# Get token from Authorization header or FastMCP context
token = _get_token_from_request(ctx)
if not token:
raise ValueError(
"OAuth authentication required but no access token available. "
"Please authenticate with FastMCP's AzureProvider."
"Please provide token via Authorization header or authenticate with FastMCP's OAuth flow."
)

# SimpleTokenAuthでトークンをラップ
auth = SimpleTokenAuth(token=access_token.token)
auth = SimpleTokenAuth(token=token)

# SharePointクライアントを作成(リクエストごと)
return SharePointSearchClient(
Expand Down Expand Up @@ -296,6 +341,7 @@ def sharepoint_docs_search(
max_results: int = 20,
file_extensions: list[str] | None = None,
response_format: str = "detailed",
ctx: Context | None = None,
) -> list[dict[str, Any]]:
"""
Search for documents in SharePoint with response format options
Expand All @@ -305,6 +351,7 @@ def sharepoint_docs_search(
max_results: Maximum number of results to return (default: 20, max: 100)
file_extensions: List of file extensions to search (e.g., ["pdf", "docx"])
response_format: Response format - "detailed" (default) or "compact"
ctx: FastMCP context (injected automatically)

Returns:
List of search results. Each result contains:
Expand All @@ -322,7 +369,7 @@ def sharepoint_docs_search(
response_format = "detailed"

try:
client = _get_sharepoint_client()
client = _get_sharepoint_client(ctx)

# ファイル拡張子のフィルタリング
if file_extensions:
Expand Down Expand Up @@ -368,20 +415,21 @@ def sharepoint_docs_search(
raise handle_sharepoint_error(e, "search") from e


def sharepoint_docs_download(file_path: str) -> str:
def sharepoint_docs_download(file_path: str, ctx: Context | None = None) -> str:
"""
Download a file from SharePoint

Args:
file_path: ダウンロードするファイルのフルパス(sharepoint_docs_searchの結果から取得)
ctx: FastMCP context (injected automatically)

Returns:
ダウンロードしたファイルの内容(Base64エンコード済み文字列)
"""
logging.info(f"Downloading SharePoint file: {file_path}")

try:
client = _get_sharepoint_client()
client = _get_sharepoint_client(ctx)

# ファイルをダウンロード
file_content = client.download_file(file_path)
Expand Down
Loading