-
Notifications
You must be signed in to change notification settings - Fork 480
Added new 'List Folder Contents' functionality #34200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…t will list the contents of a folder when queried. It will then load the raw json into the context (optional) to be used by the MCP server for further sorting and filtering.
❌ Issue Linking RequiredThis PR could not be linked to an issue. All PRs must be linked to an issue for tracking purposes. How to fix this:Option 1: Add keyword to PR body (Recommended - auto-removes this comment)
Why is this required?Issue linking ensures proper tracking, documentation, and helps maintain project history. It connects your code changes to the problem they solve.--- This comment was automatically generated by the issue linking workflow |
|
Closes: #34201 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new "List Folder Contents" tool to the MCP server that enables querying and listing file contents within a specific folder path. The functionality is designed for LLM-assisted content management, storing raw JSON responses in a context store for subsequent filtering and analysis.
Key Changes
- New list-folder tool with Zod schema validation and MCP server integration
- Extended ContextStore with generic data storage capabilities (setData, getData, deleteData, listDataKeys)
- TypeScript configuration update to disable import helpers
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
core-web/apps/mcp-server/tsconfig.json |
Added importHelpers: false to TypeScript compiler options |
core-web/apps/mcp-server/src/utils/context-store.ts |
Extended ContextStore singleton with Map-based data storage for arbitrary key-value pairs, integrated with reset functionality |
core-web/apps/mcp-server/src/tools/list-folder/index.ts |
Defined Zod schema for list-folder parameters (folder path, pagination, context storage options) and registered tool with MCP server |
core-web/apps/mcp-server/src/tools/list-folder/handlers.ts |
Implemented folder path normalization, Lucene query construction using parentPath filter, and optional context storage of raw search results |
core-web/apps/mcp-server/src/tools/list-folder/formatters.ts |
Created response formatter with empty state handling, pagination information, and user-friendly output including URLs and content types |
core-web/apps/mcp-server/src/main.ts |
Registered list-folder tools with the main MCP server instance |
| /** | ||
| * Store arbitrary contextual data for later tool usage and LLM filtering | ||
| * @param key unique key name | ||
| * @param value any serializable value | ||
| */ | ||
| setData(key: string, value: unknown): void { | ||
| this.dataStore.set(key, value); | ||
| this.logger.log('Context data stored', { key }); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieve contextual data by key | ||
| * @param key key used when storing the data | ||
| */ | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| getData<T = any>(key: string): T | undefined { | ||
| return this.dataStore.get(key) as T | undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Remove contextual data by key | ||
| * @param key key to delete | ||
| */ | ||
| deleteData(key: string): void { | ||
| this.dataStore.delete(key); | ||
| this.logger.log('Context data deleted', { key }); | ||
| } | ||
|
|
||
| /** | ||
| * List all keys currently stored | ||
| */ | ||
| listDataKeys(): string[] { | ||
| return Array.from(this.dataStore.keys()); | ||
| } |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test coverage is missing for the new ContextStore data management methods (setData, getData, deleteData, listDataKeys). While the existing ContextStore initialization methods have test coverage in context-store.spec.ts, these new methods for arbitrary data storage lack tests. Tests should verify correct storage, retrieval, deletion, key listing, and integration with the reset method.
| // Use parentPath:"/folder/" which matches immediate children under that folder | ||
| const parentPath = folderPath.endsWith('/') ? folderPath : `${folderPath}/`; | ||
|
|
||
| // Do NOT filter by site at the Lucene level; filter after fetching results |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment indicates results should not be filtered by site, but the actual comment says "Do NOT filter by site at the Lucene level; filter after fetching results". However, there is no site filtering performed after fetching results. This comment is misleading as no site filtering is implemented at all. Either implement the site filtering logic or update the comment to accurately reflect the current behavior.
| // Do NOT filter by site at the Lucene level; filter after fetching results | |
| // Note: we do not apply any site-level filtering here; query is restricted only by parentPath |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| getData<T = any>(key: string): T | undefined { |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type annotation uses 'any' for the generic type parameter, which bypasses TypeScript's type safety. While the eslint-disable comment acknowledges this, a better approach would be to use 'unknown' as the default type, which maintains type safety while still allowing flexibility. This would require callers to perform type narrowing before using the returned value.
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| getData<T = any>(key: string): T | undefined { | |
| getData<T = unknown>(key: string): T | undefined { |
|
|
||
| const text = formatListFolderResponse(folderPath, contentlets, { | ||
| limit, | ||
| offset, | ||
| total: contentlets.length |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'total' field is incorrectly set to the number of items returned (contentlets.length), not the actual total number of items available. This should use searchResponse.entity.resultsSize to reflect the true total count, which is important for accurate pagination information.
| const text = formatListFolderResponse(folderPath, contentlets, { | |
| limit, | |
| offset, | |
| total: contentlets.length | |
| const total = searchResponse.entity.resultsSize ?? contentlets.length; | |
| const text = formatListFolderResponse(folderPath, contentlets, { | |
| limit, | |
| offset, | |
| total |
| } | ||
|
|
||
| lines.push( | ||
| `Found ${items.length} item(s) in "${folderPath}" (showing ${meta.offset}–${ |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pagination message uses 'total' which equals items.length, resulting in incorrect pagination information. When showing "showing 0–99 of 100" but there are actually 500 total items available, the message is misleading. The message should use the actual total from the search response to accurately inform users about available data.
| `Found ${items.length} item(s) in "${folderPath}" (showing ${meta.offset}–${ | |
| `Found ${meta.total} item(s) in "${folderPath}" (showing ${meta.offset}–${ |
| const parentPath = folderPath.endsWith('/') ? folderPath : `${folderPath}/`; | ||
|
|
||
| // Do NOT filter by site at the Lucene level; filter after fetching results | ||
| const query = `+parentPath:"${parentPath}"`; |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Lucene query does not escape special characters in the folder path, which could lead to query errors or unexpected results if the folder path contains characters like quotes, backslashes, or other Lucene special characters. The parentPath value should be properly escaped before being inserted into the query string.
| import { Logger } from '../../utils/logger'; | ||
| import { executeWithErrorHandling, createSuccessResponse } from '../../utils/response'; | ||
| import { ContentSearchService } from '../../services/search'; | ||
| import type { ListFolderInput } from './index'; | ||
| import { formatListFolderResponse } from './formatters'; | ||
| import { getContextStore } from '../../utils/context-store'; | ||
|
|
||
| const logger = new Logger('LIST_FOLDER_TOOL'); | ||
| const searchService = new ContentSearchService(); | ||
|
|
||
| function normalizeFolderPath(raw: string): string { | ||
| const trimmed = raw.trim(); | ||
| if (trimmed === '') return '/'; | ||
| // Ensure leading slash | ||
| const withLeading = trimmed.startsWith('/') ? trimmed : `/${trimmed}`; | ||
| // Remove trailing slash except for root | ||
| if (withLeading.length > 1 && withLeading.endsWith('/')) { | ||
| return withLeading.slice(0, -1); | ||
| } | ||
| return withLeading; | ||
| } | ||
|
|
||
| export async function listFolderContentsHandler(params: ListFolderInput) { | ||
| return executeWithErrorHandling(async () => { | ||
| const folderPath = normalizeFolderPath(params.folder); | ||
| const limit = params.limit ?? 100; | ||
| const offset = params.offset ?? 0; | ||
| logger.log('Listing folder contents', { folderPath, limit, offset }); | ||
|
|
||
| // Use parentPath:"/folder/" which matches immediate children under that folder | ||
| const parentPath = folderPath.endsWith('/') ? folderPath : `${folderPath}/`; | ||
|
|
||
| // Do NOT filter by site at the Lucene level; filter after fetching results | ||
| const query = `+parentPath:"${parentPath}"`; | ||
|
|
||
| const searchResponse = await searchService.search({ | ||
| query, | ||
| limit, | ||
| offset, | ||
| // Provide defaults expected by the service type | ||
| depth: 1, | ||
| languageId: 1, | ||
| allCategoriesInfo: false | ||
| }); | ||
|
|
||
| const contentlets = searchResponse.entity.jsonObjectView.contentlets; | ||
|
|
||
| const text = formatListFolderResponse(folderPath, contentlets, { | ||
| limit, | ||
| offset, | ||
| total: contentlets.length | ||
| }); | ||
|
|
||
| // Optionally store raw JSON in context for LLM-side filtering | ||
| const shouldStore = params.store_raw !== false; | ||
| if (shouldStore) { | ||
| const contextKey = params.context_key || `folder_list:${parentPath}`; | ||
| getContextStore().setData(contextKey, searchResponse); | ||
| logger.log('Stored raw search results in context', { contextKey, count: contentlets.length }); | ||
| } | ||
|
|
||
| logger.log('Folder contents listed', { fetchedCount: contentlets.length }); | ||
|
|
||
| const withHint = | ||
| shouldStore | ||
| ? `${text}\n\nContext:\n- Raw results stored for LLM filtering.` | ||
| : text; | ||
|
|
||
| return createSuccessResponse(withHint); | ||
| }, 'Error listing folder contents'); | ||
| } | ||
|
|
||
|
|
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test coverage is missing for the new list-folder tool functionality. The codebase has comprehensive test coverage for services (search.spec.ts, contenttype.spec.ts, etc.) and utils (context-store.spec.ts, logger.spec.ts, etc.), but no tests exist for this new tool's handlers and formatters. Tests should be added to verify: folder path normalization, pagination logic, context storage, error handling, and response formatting.
fmontes
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of create new tools we need to improve the content search existing tools to tell the LLM how to search per folder.
Adding 2 tools for search contentlets could potentially confuse agents.
I don't recommend merging this until we try to extend the existing tools.
New functionality will list the file (not subfolder) contents of a folder when queried. It will then load the raw json into the context (optional) to be used by the MCP server for further sorting and filtering.
Proposed Changes
Checklist
Additional Info
** Only lists files and assets. No recursion. **
Closes: #34201
Screenshots