Skip to content
Open
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions src/tools/entries/searchEntries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,19 @@ describe('searchEntries', () => {
});
});

it('should limit search results to maximum of 3', async () => {
it('should limit search results to maximum of 1000', async () => {
const testArgs = {
...mockArgs,
query: {
limit: 10, // Should be capped to 3
limit: 1001,
},
};

const mockEntries = {
items: [],
total: 0,
skip: 0,
limit: 3,
limit: 1000,
};

mockEntryGetMany.mockResolvedValue(mockEntries);
Expand All @@ -143,7 +143,7 @@ describe('searchEntries', () => {
spaceId: testArgs.spaceId,
environmentId: testArgs.environmentId,
query: {
limit: 3,
limit: 1000,
skip: 0,
},
});
Expand All @@ -167,7 +167,7 @@ describe('searchEntries', () => {
content: [
{
type: 'text',
text: 'Error deleting dataset: Content type not found',
text: 'Error searching entries: Content type not found',
},
],
});
Expand Down
126 changes: 102 additions & 24 deletions src/tools/entries/searchEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,107 @@ import {
} from '../../utils/response.js';
import { BaseToolSchema, createToolClient } from '../../utils/tools.js';
import { summarizeData } from '../../utils/summarizer.js';
import { searchLimit } from '../../utils/limits.js';

export const SearchEntriesToolParams = BaseToolSchema.extend({
query: z.object({
content_type: z.string().optional().describe('Filter by content type'),
include: z
.number()
.optional()
.describe('Include this many levels of linked entries'),
select: z
.string()
.optional()
.describe('Comma-separated list of fields to return'),
links_to_entry: z
.string()
.optional()
.describe('Find entries that link to the specified entry ID'),
limit: z
.number()
.optional()
.describe('Maximum number of entries to return'),
skip: z.number().optional().describe('Skip this many entries'),
order: z.string().optional().describe('Order entries by this field'),
}),
query: z
.object({
// Core parameters (maintain backward compatibility)
content_type: z.string().optional().describe('Filter by content type'),
include: z
.number()
.optional()
.describe('Include this many levels of linked entries'),
select: z
.string()
.optional()
.describe('Comma-separated list of fields to return'),
links_to_entry: z
.string()
.optional()
.describe('Find entries that link to the specified entry ID'),
limit: z
.number()
.optional()
.describe(
'Maximum number of entries to return (default: 10, max: 100)',
),
skip: z
.number()
.optional()
.describe('Skip this many entries for pagination'),
order: z.string().optional().describe('Order entries by this field'),

// Full-text search
query: z
.string()
.optional()
.describe('Full-text search across all fields'),

// Common field-based searches (examples - any field is supported via catchall)
'fields.title': z.string().optional().describe('Search by title field'),
'fields.slug': z.string().optional().describe('Search by slug field'),
'fields.internalName': z
.string()
.optional()
.describe('Search by internal name field'),
'fields.text': z
.string()
.optional()
.describe('Search by text field (useful for testimonials)'),
'fields.title[match]': z
.string()
.optional()
.describe('Pattern match on title field'),
'fields.slug[match]': z
.string()
.optional()
.describe('Pattern match on slug field'),
'fields.title[exists]': z
.boolean()
.optional()
.describe('Check if title field exists'),
'fields.slug[exists]': z
.boolean()
.optional()
.describe('Check if slug field exists'),

// System field searches
'sys.id[in]': z
.array(z.string())
.optional()
.describe('Search by multiple entry IDs'),
'sys.contentType.sys.id': z
.string()
.optional()
.describe('Filter by content type ID'),
'sys.createdAt[gte]': z
.string()
.optional()
.describe('Created after date (ISO format)'),
'sys.createdAt[lte]': z
.string()
.optional()
.describe('Created before date (ISO format)'),
'sys.updatedAt[gte]': z
.string()
.optional()
.describe('Updated after date (ISO format)'),
'sys.updatedAt[lte]': z
.string()
.optional()
.describe('Updated before date (ISO format)'),

// Metadata searches
'metadata.tags.sys.id[in]': z
.array(z.string())
.optional()
.describe('Filter by tag IDs'),
})
.catchall(z.any())
.describe(
'Flexible search parameters supporting ANY Contentful API query parameter. Use fields.* for field searches, sys.* for system fields, and any other Contentful API parameter.',
),
});

type Params = z.infer<typeof SearchEntriesToolParams>;
Expand All @@ -44,13 +122,13 @@ async function tool(args: Params) {
...params,
query: {
...args.query,
limit: Math.min(args.query.limit || 3, 3),
limit: searchLimit(args.query.limit),
skip: args.query.skip || 0,
},
});

const summarized = summarizeData(entries, {
maxItems: 3,
maxItems: searchLimit(args.query.limit),
remainingMessage:
'To see more entries, please ask me to retrieve the next page.',
});
Expand All @@ -62,5 +140,5 @@ async function tool(args: Params) {

export const searchEntriesTool = withErrorHandling(
tool,
'Error deleting dataset',
'Error searching entries',
);
3 changes: 3 additions & 0 deletions src/utils/limits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function searchLimit(userLimit?: number) {
return Math.min(userLimit || 10, 1000);
}