Skip to content

feat: add sharepoint_list_folder tool for direct folder browsing (#65)#66

Open
k-ibaraki wants to merge 3 commits into
mainfrom
feature/sharepoint-list-folder
Open

feat: add sharepoint_list_folder tool for direct folder browsing (#65)#66
k-ibaraki wants to merge 3 commits into
mainfrom
feature/sharepoint-list-folder

Conversation

@k-ibaraki

Copy link
Copy Markdown
Member

Summary

背景

SharePoint の検索インデックスが一部ファイルを検知できないケースがあり、直接フォルダを参照できる機能が必要だった。

変更内容

新規ツール: sharepoint_list_folder

パラメータ デフォルト 説明
folder_path str 必須 フォルダのフルURL or サーバー相対URL
max_results int 100 1ページあたりの最大ファイル数(上限500)
next_token str | None None 継続トークン(前回レスポンスから)

レスポンス:

{
  "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.pylist_folder() メソッド追加
  • src/server.pysharepoint_list_folder() ツール関数と register_tools() への登録
  • src/error_messages.pyFOLDER_NOT_FOUND エラーカテゴリ追加
  • src/config.pydisabled_tools docstring に新ツール名を追記
  • tests/test_server.py — ツール登録テストのカウント更新
  • README.md / README_ja.md — Features セクションに新ツール追記
  • docs/usage.md / docs/usage_ja.md — フォルダ一覧セクション追加
  • CHANGELOG.md[Unreleased] セクションに追記

Test plan

  • 型チェック(ty)パス
  • Lint(ruff)パス
  • テスト(pytest)パス
  • 実環境での sharepoint_list_folder 動作確認(特にページング)

🤖 Generated with Claude Code

k-ibaraki and others added 3 commits June 8, 2026 14:55
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>
Copilot AI review requested due to automatic review settings June 8, 2026 07:41

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/sharepoint_search.py
Comment on lines +292 to +293
if next_token:
files_resp = requests.get(next_token, headers=headers, timeout=30)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

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)

Comment thread src/sharepoint_search.py
Comment on lines +222 to +229
# フル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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

folder_path に先頭のスラッシュがないサーバー相対パス(例: sites/mysite/Shared Documents)が渡された場合、server_relative_url.split("/") の結果のインデックスがずれ、is_onedriveapi_base_url の判定が正しく行われなくなります。

これにより、本来 /sites/mysite であるべき api_base_url がルートテナントの URL にフォールバックしてしまい、API リクエストが 404 エラーなどで失敗するバグが発生します。

また、末尾にスラッシュがある場合も SharePoint API がエラーを返す可能性があるため、先頭と末尾のスラッシュを正規化することをお勧めします。

Suggested change
# フル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}"

Comment thread tests/test_server.py
Comment on lines 444 to 449
register_tools()

# mcp.tool が3回呼ばれることを確認
# ※ローカルの.envにSHAREPOINT_DISABLED_TOOLS=sharepoint_excelが設定されているため
# excelは除外され、search + download + list_folderの3ツールが登録される
assert mock_mcp.tool.call_count == 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

test_all_tools_registered_by_default テストがローカル開発環境の .env ファイルに依存してしまっており、CI/CD 環境でテストが失敗する原因になります。

patch.dictos.environ をクリアしていますが、reload(src.config) 内で load_dotenv() が再度実行されるため、ローカルの .envSHAREPOINT_DISABLED_TOOLS=sharepoint_excel が設定されていると、それが読み込まれてアサーションが 3 でパスしてしまいます。
しかし、CI/CD 環境など .env が存在しない環境では、新規追加された sharepoint_list_folder を含む全4ツールが登録されるため、call_count == 3 のアサーションが失敗します。

テスト内で明示的に _disabled_tools_str を空文字に設定し、アサーションを 4 に修正することで、テスト環境を完全に分離(hermetic)してください。

Suggested change
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

Comment thread src/server.py

try:
client = _get_sharepoint_client(ctx)
max_results = min(max_results, 500)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

max_results0 や負の数が指定された場合に SharePoint API がエラーを返すのを防ぐため、下限値を 1 に制限する防衛的プログラミングを行うことをお勧めします。

Suggested change
max_results = min(max_results, 500)
max_results = max(1, min(max_results, 500))

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

sharepoint_docs_search のインデックス問題を回避するために、フォルダを直接ブラウズできる sharepoint_list_folder ツールを追加した点は、Issue #65 の背景に対して実用的で前向きな改善です。

Changes:

  • 検索インデックスを介さず SharePoint/OneDrive フォルダ配下を直接列挙する sharepoint_list_folder を追加
  • $skiptokend.__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.

Comment thread src/sharepoint_search.py
Comment on lines +268 to +270
if next_token is None:
folders_url = f"{api_base_url}/_api/web/GetFolderByServerRelativeUrl('{escaped_path}')/Folders"
folders_resp = requests.get(
Comment thread src/sharepoint_search.py
Comment on lines +292 to +306
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()
Comment thread src/server.py
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.
Comment thread docs/usage.md
| 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. |
Comment thread docs/usage_ja.md
| パラメータ | 型 | デフォルト | 説明 |
|-----------|------|---------|-------------|
| `folder_path` | str | 必須 | フォルダのフルURLまたはサーバー相対URL |
| `max_results` | int | 100 | 1ページあたりの最大ファイル数(上限500)。サブフォルダは常に全件返却。 |
Comment thread tests/test_server.py
Comment on lines 446 to 449
# mcp.tool が3回呼ばれることを確認
# ※ローカルの.envにSHAREPOINT_DISABLED_TOOLS=sharepoint_excelが設定されているため
# excelは除外され、search + download + list_folderの3ツールが登録される
assert mock_mcp.tool.call_count == 3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants