feat: add sharepoint_list_folder tool for direct folder browsing (#65)#66
feat: add sharepoint_list_folder tool for direct folder browsing (#65)#66k-ibaraki wants to merge 3 commits into
Conversation
Addresses issue #65 - enables browsing folder contents directly via SharePoint REST API, bypassing the search index which sometimes misses files. - New tool: sharepoint_list_folder - Lists subfolders (all) and files (paginated) in a given folder - Supports both full URL and server-relative URL input - Handles SharePoint sites and OneDrive paths automatically - Returns name, path, size, modified for each item - Pagination via max_results + skip parameters - File paths are compatible with sharepoint_docs_download - Added FOLDER_NOT_FOUND error category and "list" context handling - Updated disabled_tools documentation to include new tool name - Updated tests to reflect new tool count Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_list_folder $skip on SharePoint /Files navigation property is unreliable; switch to SharePoint's odata=verbose d.__next continuation token ($skiptoken) for correct pagination across all SharePoint Online deployments. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the sharepoint_list_folder tool, allowing users to list files and subfolders directly from SharePoint or OneDrive by bypassing the search index. It includes implementation details, documentation updates, and test adjustments. The code review identified several critical issues: a potential SSRF and credential leakage vulnerability due to unvalidated next_token URLs, a path-parsing bug when server-relative paths lack a leading slash, a test isolation issue where tests depend on local .env files and will fail in CI/CD, and a recommendation to enforce a minimum value of 1 for max_results to prevent API errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if next_token: | ||
| files_resp = requests.get(next_token, headers=headers, timeout=30) |
There was a problem hiding this comment.
next_token に任意の外部 URL が指定された場合、その URL に対して Authorization ヘッダー(アクセストークン)を含んだリクエストが送信されてしまうため、SSRF(Server-Side Request Forgery) および 認証情報の漏洩(Credential Leakage) の脆弱性があります。
悪意のあるユーザーや LLM が next_token に攻撃者のサーバーを指定することで、SharePoint のアクセストークンが盗まれる危険性があります。
next_token のドメインが、信頼された SharePoint または OneDrive のドメイン(global_config.base_url)と一致することを確認するバリデーションを追加してください。
if next_token:
# SSRFおよびアクセストークンの漏洩を防ぐため、ドメインを検証
parsed_token = urlparse(next_token)
base_netloc = urlparse(global_config.base_url).netloc
allowed_domains = {
base_netloc,
base_netloc.replace(".sharepoint.com", "-my.sharepoint.com")
}
if parsed_token.netloc not in allowed_domains or parsed_token.scheme != "https":
raise ValueError("Invalid next_token: Domain or scheme is not allowed.")
files_resp = requests.get(next_token, headers=headers, timeout=30)| # フルURLとサーバー相対URLの両方を受け付ける | ||
| parsed_url = urlparse(folder_path) | ||
| if parsed_url.scheme: | ||
| server_relative_url = unquote(parsed_url.path) | ||
| domain = f"{parsed_url.scheme}://{parsed_url.netloc}" | ||
| else: | ||
| server_relative_url = folder_path | ||
| domain = None |
There was a problem hiding this comment.
folder_path に先頭のスラッシュがないサーバー相対パス(例: sites/mysite/Shared Documents)が渡された場合、server_relative_url.split("/") の結果のインデックスがずれ、is_onedrive や api_base_url の判定が正しく行われなくなります。
これにより、本来 /sites/mysite であるべき api_base_url がルートテナントの URL にフォールバックしてしまい、API リクエストが 404 エラーなどで失敗するバグが発生します。
また、末尾にスラッシュがある場合も SharePoint API がエラーを返す可能性があるため、先頭と末尾のスラッシュを正規化することをお勧めします。
| # フルURLとサーバー相対URLの両方を受け付ける | |
| parsed_url = urlparse(folder_path) | |
| if parsed_url.scheme: | |
| server_relative_url = unquote(parsed_url.path) | |
| domain = f"{parsed_url.scheme}://{parsed_url.netloc}" | |
| else: | |
| server_relative_url = folder_path | |
| domain = None | |
| # フルURLとサーバー相対URLの両方を受け付ける | |
| parsed_url = urlparse(folder_path) | |
| if parsed_url.scheme: | |
| server_relative_url = unquote(parsed_url.path) | |
| domain = f"{parsed_url.scheme}://{parsed_url.netloc}" | |
| else: | |
| server_relative_url = folder_path | |
| domain = None | |
| # 先頭と末尾のスラッシュを正規化 | |
| server_relative_url = server_relative_url.rstrip("/") | |
| if not server_relative_url.startswith("/"): | |
| server_relative_url = f"/{server_relative_url}" |
| register_tools() | ||
|
|
||
| # mcp.tool が3回呼ばれることを確認 | ||
| # ※ローカルの.envにSHAREPOINT_DISABLED_TOOLS=sharepoint_excelが設定されているため | ||
| # excelは除外され、search + download + list_folderの3ツールが登録される | ||
| assert mock_mcp.tool.call_count == 3 |
There was a problem hiding this comment.
test_all_tools_registered_by_default テストがローカル開発環境の .env ファイルに依存してしまっており、CI/CD 環境でテストが失敗する原因になります。
patch.dict で os.environ をクリアしていますが、reload(src.config) 内で load_dotenv() が再度実行されるため、ローカルの .env に SHAREPOINT_DISABLED_TOOLS=sharepoint_excel が設定されていると、それが読み込まれてアサーションが 3 でパスしてしまいます。
しかし、CI/CD 環境など .env が存在しない環境では、新規追加された sharepoint_list_folder を含む全4ツールが登録されるため、call_count == 3 のアサーションが失敗します。
テスト内で明示的に _disabled_tools_str を空文字に設定し、アサーションを 4 に修正することで、テスト環境を完全に分離(hermetic)してください。
| register_tools() | |
| # mcp.tool が3回呼ばれることを確認 | |
| # ※ローカルの.envにSHAREPOINT_DISABLED_TOOLS=sharepoint_excelが設定されているため | |
| # excelは除外され、search + download + list_folderの3ツールが登録される | |
| assert mock_mcp.tool.call_count == 3 | |
| src.config.config._disabled_tools_str = "" | |
| register_tools() | |
| # 全4ツールが登録されることを確認 | |
| assert mock_mcp.tool.call_count == 4 |
|
|
||
| try: | ||
| client = _get_sharepoint_client(ctx) | ||
| max_results = min(max_results, 500) |
There was a problem hiding this comment.
Pull request overview
sharepoint_docs_search のインデックス問題を回避するために、フォルダを直接ブラウズできる sharepoint_list_folder ツールを追加した点は、Issue #65 の背景に対して実用的で前向きな改善です。
Changes:
- 検索インデックスを介さず SharePoint/OneDrive フォルダ配下を直接列挙する
sharepoint_list_folderを追加 $skiptoken(d.__next)をnext_tokenとして扱うページングを導入- ドキュメント/Changelog/ツール登録テストを更新
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/sharepoint_search.py |
SharePoint REST API を使ったフォルダ内容の直接列挙(ページング対応)を追加 |
src/server.py |
MCP ツール関数 sharepoint_list_folder の追加とツール登録を拡張 |
src/error_messages.py |
フォルダ未検出時のエラーカテゴリとハンドリングを追加 |
src/config.py |
無効化ツール一覧の docstring に sharepoint_list_folder を追記 |
tests/test_server.py |
ツール登録数の期待値を更新 |
README.md / README_ja.md |
Features に sharepoint_list_folder を追記 |
docs/usage.md / docs/usage_ja.md |
フォルダ一覧の使用例セクションを追加 |
CHANGELOG.md |
Unreleased に新ツール追加を記載 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if next_token is None: | ||
| folders_url = f"{api_base_url}/_api/web/GetFolderByServerRelativeUrl('{escaped_path}')/Folders" | ||
| folders_resp = requests.get( |
| if next_token: | ||
| files_resp = requests.get(next_token, headers=headers, timeout=30) | ||
| else: | ||
| files_url = f"{api_base_url}/_api/web/GetFolderByServerRelativeUrl('{escaped_path}')/Files" | ||
| files_resp = requests.get( | ||
| files_url, | ||
| params={ | ||
| "$select": "Name,ServerRelativeUrl,Length,TimeLastModified", | ||
| "$top": max_results, | ||
| "$orderby": "Name", | ||
| }, | ||
| headers=headers, | ||
| timeout=30, | ||
| ) | ||
| files_resp.raise_for_status() |
| Examples: | ||
| https://company.sharepoint.com/sites/mysite/Shared Documents/Reports | ||
| /sites/mysite/Shared Documents/Reports | ||
| max_results: Max files per page (default: 100, max: 500). Folders are always returned in full. |
| | Parameter | Type | Default | Description | | ||
| |-----------|------|---------|-------------| | ||
| | `folder_path` | str | Required | Full URL or server-relative URL of the folder | | ||
| | `max_results` | int | 100 | Max files per page (capped at 500). Subfolders are always returned in full. | |
| | パラメータ | 型 | デフォルト | 説明 | | ||
| |-----------|------|---------|-------------| | ||
| | `folder_path` | str | 必須 | フォルダのフルURLまたはサーバー相対URL | | ||
| | `max_results` | int | 100 | 1ページあたりの最大ファイル数(上限500)。サブフォルダは常に全件返却。 | |
| # mcp.tool が3回呼ばれることを確認 | ||
| # ※ローカルの.envにSHAREPOINT_DISABLED_TOOLS=sharepoint_excelが設定されているため | ||
| # excelは除外され、search + download + list_folderの3ツールが登録される | ||
| assert mock_mcp.tool.call_count == 3 |
Summary
sharepoint_list_folderMCP ツールを追加(Issue [FEATURE] ファイルのリスト表示機能が欲しい #65)$skiptokenベースの継続トークン(next_token)によるページング背景
SharePoint の検索インデックスが一部ファイルを検知できないケースがあり、直接フォルダを参照できる機能が必要だった。
変更内容
新規ツール:
sharepoint_list_folderfolder_pathmax_resultsnext_tokenレスポンス:
{ "folders": [{"name": "...", "path": "...", "modified": "..."}], "files": [{"name": "...", "path": "...", "size": 0, "modified": "..."}], "next_token": null }設計上のポイント
$skip(信頼性リスクあり)ではなく、SharePoint odata=verbose レスポンスのd.__nextフィールド($skiptoken)を使用sharepoint_docs_downloadに渡せるSHAREPOINT_DISABLED_TOOLS=sharepoint_list_folderで無効化可能変更ファイル
src/sharepoint_search.py—list_folder()メソッド追加src/server.py—sharepoint_list_folder()ツール関数とregister_tools()への登録src/error_messages.py—FOLDER_NOT_FOUNDエラーカテゴリ追加src/config.py—disabled_toolsdocstring に新ツール名を追記tests/test_server.py— ツール登録テストのカウント更新README.md/README_ja.md— Features セクションに新ツール追記docs/usage.md/docs/usage_ja.md— フォルダ一覧セクション追加CHANGELOG.md—[Unreleased]セクションに追記Test plan
sharepoint_list_folder動作確認(特にページング)🤖 Generated with Claude Code