Skip to content

feat: Add tools for get items from collections paginated and get items#10

Merged
vrag99 merged 6 commits intomainfrom
amx/mcp
Apr 13, 2026
Merged

feat: Add tools for get items from collections paginated and get items#10
vrag99 merged 6 commits intomainfrom
amx/mcp

Conversation

@asutosh29
Copy link
Copy Markdown
Contributor

No description provided.

Copy link
Copy Markdown

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 new service methods and MCP server tools to retrieve items from collections with cursor-based pagination, and to retrieve multiple items by ID, enabling more efficient “fetch many” workflows for clients consuming the MCP interface.

Changes:

  • Added ItemService.getItemsByIds() to fetch multiple items with an access check via collection membership.
  • Extended CollectionService.getCollection() to support cursor/limit pagination and return pagination metadata.
  • Registered two new MCP tools: get_collection_items (paginated) and get_items_by_id.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
apps/server/src/services/item.service.ts Adds a multi-ID item fetch with collection-based access validation.
apps/server/src/services/collection.service.ts Implements cursor pagination (limit + cursor) and returns pagination metadata.
apps/server/src/mcp/server.ts Exposes new MCP tools for paginated collection reads and multi-ID item reads.
.vscode/settings.json Updates workspace TypeScript SDK configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +163 to +191
async getItemsByIds(userId: string, itemIds: string[]) {
if (!itemIds || itemIds.length === 0) return [];

const results = await db
.select({
...getTableColumns(itemsTable),
})
.from(itemsTable)
.where(
and(
inArray(itemsTable.id, itemIds),
exists(
db
.select()
.from(collectionItemsTable)
.innerJoin(
userCollectionsTable,
and(
eq(
userCollectionsTable.collectionId,
collectionItemsTable.collectionId,
),
eq(userCollectionsTable.userId, userId),
),
)
.where(eq(collectionItemsTable.itemId, itemsTable.id)),
),
),
);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Security/access-scope: this query only checks that the item is in any collection the user belongs to. For MCP API keys in collection_specific mode, this would allow fetching items from collections outside the key’s granted scope (since it’s only keyed by userId). Consider adding an optional allowedCollectionIds (or similar) parameter and include it in the exists join condition (e.g., restrict collectionItemsTable.collectionId / userCollectionsTable.collectionId) so callers can enforce API-key scoping at the data layer.

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +135
// itemsTable.createdAt is descending, so we want items older (<) than the cursor
queryConditions.push(lt(itemsTable.createdAt, new Date(cursor)));
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Cursor handling: new Date(cursor) will silently produce an Invalid Date for malformed input, which can lead to incorrect filtering or runtime issues when building the SQL. Prefer validating/parsing the cursor (e.g., reject invalid dates) and/or change the API to accept a Date (or enforce an ISO datetime string at validation boundaries) before constructing the query.

Suggested change
// itemsTable.createdAt is descending, so we want items older (<) than the cursor
queryConditions.push(lt(itemsTable.createdAt, new Date(cursor)));
const cursorDate = new Date(cursor);
if (Number.isNaN(cursorDate.getTime())) {
throw new Error("Invalid cursor: expected an ISO 8601 datetime string");
}
// itemsTable.createdAt is descending, so we want items older (<) than the cursor
queryConditions.push(lt(itemsTable.createdAt, cursorDate));

Copilot uses AI. Check for mistakes.
Comment on lines 138 to +162
@@ -128,13 +144,31 @@ export class CollectionService {
collectionItemsTable,
eq(itemsTable.id, collectionItemsTable.itemId),
)
.where(eq(collectionItemsTable.collectionId, collectionId))
.where(and(...queryConditions))
.orderBy(desc(itemsTable.createdAt));

// Fetch items ordered by createdAt (newest first). Add +1 to check for next page.
const items = limit ? await baseQuery.limit(limit + 1) : await baseQuery;

let hasMore = false;
let nextCursor = null;

if (limit && items.length > limit) {
hasMore = true;
items.pop(); // Remove the extra item
// Get the createdAt from the new last item for the next cursor
const lastItem = items[items.length - 1];
nextCursor = lastItem ? lastItem.createdAt.toISOString() : null;
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Pagination correctness: using only createdAt as the cursor with orderBy(createdAt DESC) can skip or duplicate items when multiple rows share the same createdAt (common under high write concurrency). To make pagination stable, include a tie-breaker in both ordering and cursor (e.g., (createdAt, id) with orderBy(createdAt DESC, id DESC) and a cursor containing both values).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

also, we are using uuid-7, vo time based sort ho jaati h @asutosh29, can use that here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok will keep that in mind while making the changes

Comment on lines +321 to +357
server.registerTool(
"get_collection_items",
{
description:
"Get items present within a specific collection using safe pagination",
inputSchema: z.object({
collectionId: z
.string()
.describe("ID of the collection to fetch items from"),
limit: z
.number()
.min(1)
.max(50)
.optional()
.describe(
"Number of items to fetch. Min: 1, Max: 50. Recommended: 10-25.",
),
cursor: z
.string()
.optional()
.describe(
"Pagination cursor (ISO date string) returned from a previous call. Do not provide on the first call.",
),
}),
},
async ({ collectionId, limit, cursor }) => {
const { actor } = getMcpContext();

try {
const fetchLimit = limit || 25; // Default safe limit
const collection = await collectionService.getCollection(
collectionId,
actor.userId,
fetchLimit,
cursor,
);

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Access control: this tool does not enforce actor.mode === "collection_specific" scoping the way add_item_to_collection does. As a result, an API key granted to only some collections could still fetch items from any collection the owning user belongs to by guessing collectionId (the service layer only checks userId). Add an explicit hasAccess check against actor.grantedCollections/actor.userCollections before calling collectionService.getCollection.

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +344
inputSchema: z.object({
collectionId: z
.string()
.describe("ID of the collection to fetch items from"),
limit: z
.number()
.min(1)
.max(50)
.optional()
.describe(
"Number of items to fetch. Min: 1, Max: 50. Recommended: 10-25.",
),
cursor: z
.string()
.optional()
.describe(
"Pagination cursor (ISO date string) returned from a previous call. Do not provide on the first call.",
),
}),
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Input validation: cursor is documented as an ISO date string but the schema accepts any string. Use Zod datetime validation (e.g., z.string().datetime()) to fail fast on malformed cursors instead of letting new Date(cursor) through to the DB query.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

schema validation for pagination is necessary and a good practice.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Added .uuid() validation to the cursor field in the get_collection_items schema (commit f41e67b). Since we migrated to UUIDv7-based cursors, UUID format validation is the right constraint here — invalid cursors are now rejected at the schema level before reaching the DB query.

Comment thread apps/server/src/mcp/server.ts Outdated
Comment on lines +401 to +418
server.registerTool(
"get_items_by_id",
{
description:
"Retrieve specific items by their IDs. Only specify IDs you are absolutely sure of. If you need to find IDs, use other available tools.",
inputSchema: z.object({
itemIds: z
.array(z.string())
.describe(
"List of item IDs to retrieve. Only provide valid IDs that you have obtained from other tools.",
),
}),
},
async ({ itemIds }) => {
const { actor } = getMcpContext();

try {
const items = await itemService.getItemsByIds(actor.userId, itemIds);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Security/performance: itemIds is unbounded and not scoped to the API key’s granted collections. A large array could produce very expensive IN (...) queries, and a collection_specific key could retrieve items outside its allowed collection scope (the underlying query only checks userId). Add schema limits (e.g., .min(1).max(N)) and enforce access scope (either by passing allowed collection IDs into getItemsByIds or by rejecting IDs not in the key’s granted collections).

Copilot uses AI. Check for mistakes.
Comment thread apps/server/src/mcp/server.ts Outdated
id: collection.id,
title: collection.title,
role: collection.role,
itemCount: collection.items.length,
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Response field naming: itemCount here is the count of items returned in the current page, not the total number of items in the collection. This can be misleading to tool consumers; consider renaming to resultCount/pageSize or adding a separate total count if that’s what you intend to expose.

Suggested change
itemCount: collection.items.length,
resultCount: collection.items.length,

Copilot uses AI. Check for mistakes.
@vrag99
Copy link
Copy Markdown
Collaborator

vrag99 commented Apr 13, 2026

@copilot review

@vrag99
Copy link
Copy Markdown
Collaborator

vrag99 commented Apr 13, 2026

Good work. Approved

@vrag99 vrag99 merged commit 04b51fc into main Apr 13, 2026
1 check passed
@vrag99 vrag99 deleted the amx/mcp branch April 13, 2026 17:47
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.

4 participants