Skip to content

Conversation

@Laura-dotCMS
Copy link

@Laura-dotCMS Laura-dotCMS commented Jan 5, 2026

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

  • List Folder Contents function

Checklist

  • Tests
  • Translations
  • Security Implications Contemplated (add notes if applicable)

Additional Info

** Only lists files and assets. No recursion. **
Closes: #34201

Screenshots

image

…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.
@github-actions
Copy link

github-actions bot commented Jan 5, 2026

❌ Issue Linking Required

This 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)
Edit this PR description and add one of these lines:

  • This PR fixes #123 or Fixes: #123

  • This PR closes #123 or Closes: #123

  • This PR resolves #123 or Resolves: #123

  • Other supported keywords: fix, fixed, close, closed, resolve, resolved
    Option 2: Link via GitHub UI (Note: won't clear the failed check)

  1. Go to the PR → Development section (right sidebar)

  2. Click "Link issue" and select an existing issue

  3. Push a new commit or re-run the workflow to clear the failed check
    Option 3: Use branch naming
    Create a new branch with one of these patterns:

  • 123-feature-description (number at start)

  • issue-123-feature-description (issue-number at start)

  • feature-issue-123 (issue-number anywhere)

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

@Laura-dotCMS
Copy link
Author

Closes: #34201

@Laura-dotCMS Laura-dotCMS linked an issue Jan 5, 2026 that may be closed by this pull request
4 tasks
Copy link
Contributor

Copilot AI left a 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

Comment on lines +107 to +140
/**
* 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());
}
Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.
// 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
Copy link

Copilot AI Jan 5, 2026

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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +122
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getData<T = any>(key: string): T | undefined {
Copy link

Copilot AI Jan 5, 2026

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.

Suggested change
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getData<T = any>(key: string): T | undefined {
getData<T = unknown>(key: string): T | undefined {

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +51

const text = formatListFolderResponse(folderPath, contentlets, {
limit,
offset,
total: contentlets.length
Copy link

Copilot AI Jan 5, 2026

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.

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

Copilot uses AI. Check for mistakes.
}

lines.push(
`Found ${items.length} item(s) in "${folderPath}" (showing ${meta.offset}${
Copy link

Copilot AI Jan 5, 2026

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.

Suggested change
`Found ${items.length} item(s) in "${folderPath}" (showing ${meta.offset}–${
`Found ${meta.total} item(s) in "${folderPath}" (showing ${meta.offset}–${

Copilot uses AI. Check for mistakes.
const parentPath = folderPath.endsWith('/') ? folderPath : `${folderPath}/`;

// Do NOT filter by site at the Lucene level; filter after fetching results
const query = `+parentPath:"${parentPath}"`;
Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +73
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');
}


Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Member

@fmontes fmontes left a 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.

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.

[FEATURE] MCP Server List Folder Contents

3 participants