search-museum-objects
Search the Met Museum collection. Returns total object count and paginated object IDs. Filter by title, images, highlights, on-view status, artist, medium, location, date range, and more.
Instructions
Search for objects in the Metropolitan Museum of Art (Met Museum). Will return Total objects found, followed by a paginated list of Object Ids. If the Met Explorer app (open-met-explorer) is open and the user is referring to its existing results, prefer using those results from context instead of calling this tool. The parameter title should be set to true if you want to search for objects by title. The parameter hasImages is false by default, but can be set to true to return only objects with images. Additional optional filters are available for highlights, tags, on-view status, artist/culture match, medium, geographic location, and date range. Use page and pageSize to paginate results.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| q | Yes | The search query, Returns a listing of all Object IDs for objects that contain the search query within the object's data | |
| hasImages | No | Only returns objects that have images | |
| title | No | This should be set to true if you want to search for objects by title | |
| isHighlight | No | Only returns objects designated as highlights | |
| tags | No | Only returns objects that have subject keyword tags | |
| isOnView | No | Only returns objects currently on view | |
| artistOrCulture | No | When true, q is matched against artist or culture | |
| departmentId | No | Returns objects that are in the specified department. The departmentId should come from the 'list-departments' tool. | |
| medium | No | Restricts search to objects with the specified medium | |
| geoLocation | No | Restricts search to objects with the specified geographic location | |
| dateBegin | No | Start year for a date range filter. Must be provided together with dateEnd. | |
| dateEnd | No | End year for a date range filter. Must be provided together with dateBegin. | |
| page | No | 1-based page number for paginated object IDs | |
| pageSize | No | Number of object IDs to return per page (max 100) |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| total | Yes | Total number of matching objects across all pages | |
| page | Yes | Current 1-based page number returned by the tool | |
| pageSize | Yes | Number of object IDs returned per page | |
| totalPages | Yes | Total number of pages available for this query | |
| objectIDs | Yes | Paginated list of object IDs for the current page |
Implementation Reference
- The SearchMuseumObjectsTool class contains the execute() method (line 62-160) that implements the core tool logic: parses input, calls the API client's searchObjects, paginates results, and returns object IDs with structured content.
export class SearchMuseumObjectsTool { // Define public tool properties public readonly name: string = 'search-museum-objects'; public readonly description: string = 'Search for objects in the Metropolitan Museum of Art (Met Museum). Will return Total objects found, ' + 'followed by a paginated list of Object Ids. ' + 'If the Met Explorer app (open-met-explorer) is open and the user is referring to its existing results, prefer using those results from context instead of calling this tool. ' + 'The parameter title should be set to true if you want to search for objects by title. ' + 'The parameter hasImages is false by default, but can be set to true to return only objects with images. ' + 'Additional optional filters are available for highlights, tags, on-view status, artist/culture match, medium, ' + 'geographic location, and date range. ' + 'Use page and pageSize to paginate results.'; // Define the MCP registration schema (must stay a ZodObject because server registration reads `.shape`). public readonly inputSchema = SearchInputBaseSchema; private readonly apiClient: MetMuseumApiClient; constructor(apiClient: MetMuseumApiClient) { this.apiClient = apiClient; } /** * Execute the search operation */ public async execute(input: z.infer<typeof this.inputSchema>): Promise<CallToolResult> { try { const parsedInput = SearchInputSchema.safeParse(input); if (!parsedInput.success) { const validationMessage = parsedInput.error.issues .map((issue) => { const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''; return `${path}${issue.message}`; }) .join(' '); return { content: [{ type: 'text', text: validationMessage || 'Invalid search input.', }], isError: true, }; } const { q, hasImages, title, isHighlight, tags, isOnView, artistOrCulture, departmentId, medium, geoLocation, dateBegin, dateEnd, page, pageSize, } = parsedInput.data; const searchResult = await this.apiClient.searchObjects({ q, hasImages, title, isHighlight, tags, isOnView, artistOrCulture, departmentId, medium, geoLocation, dateBegin, dateEnd, }); if (searchResult.total === 0 || !searchResult.objectIDs) { const structuredContent: SearchMuseumObjectsStructuredContent = { total: 0, page: 1, pageSize, totalPages: 0, objectIDs: [], }; return { content: [{ type: 'text', text: 'No objects found' }], structuredContent, isError: false, }; } const total = searchResult.total; const allObjectIds = searchResult.objectIDs; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const safePage = Math.min(page, totalPages); const start = (safePage - 1) * pageSize; const objectIDs = allObjectIds.slice(start, start + pageSize); const text = `Total objects found: ${total}\nPage: ${safePage}/${totalPages}\nObject IDs: ${objectIDs.join(', ')}`; const structuredContent: SearchMuseumObjectsStructuredContent = { total, page: safePage, pageSize, totalPages, objectIDs, }; return { content: [{ type: 'text', text }], structuredContent, isError: false, }; } catch (error) { // Note: Error is returned to user in the tool response below. // No need to log to stderr as it would leak implementation details in stdio mode. const message = error instanceof MetMuseumApiError && error.isUserFriendly ? error.message : `Error searching museum objects: ${error}`; return { content: [{ type: 'text', text: message }], isError: true, }; } } } - Zod schemas defining the input parameters for search-museum-objects: q, hasImages, title, isHighlight, tags, isOnView, artistOrCulture, departmentId, medium, geoLocation, dateBegin/dateEnd, page, pageSize — with validation that dateBegin/dateEnd are provided together.
const SearchInputBaseSchema = z.object({ q: z.string().trim().min(1, 'The search query (q) must not be empty.').describe(`The search query, Returns a listing of all Object IDs for objects that contain the search query within the object's data`), hasImages: z.boolean().optional().default(false).describe(`Only returns objects that have images`), title: z.boolean().optional().default(false).describe(`This should be set to true if you want to search for objects by title`), isHighlight: z.boolean().optional().default(false).describe(`Only returns objects designated as highlights`), tags: z.boolean().optional().default(false).describe(`Only returns objects that have subject keyword tags`), isOnView: z.boolean().optional().default(false).describe(`Only returns objects currently on view`), artistOrCulture: z.boolean().optional().default(false).describe(`When true, q is matched against artist or culture`), departmentId: z.number().optional().describe(`Returns objects that are in the specified department. The departmentId should come from the 'list-departments' tool.`), medium: z.string().optional().describe(`Restricts search to objects with the specified medium`), geoLocation: z.string().optional().describe(`Restricts search to objects with the specified geographic location`), dateBegin: z.number().int().optional().describe(`Start year for a date range filter. Must be provided together with dateEnd.`), dateEnd: z.number().int().optional().describe(`End year for a date range filter. Must be provided together with dateBegin.`), page: z.number().int().positive().optional().default(1).describe(`1-based page number for paginated object IDs`), pageSize: z.number().int().positive().max(MAX_SEARCH_PAGE_SIZE).optional().default(DEFAULT_SEARCH_TOOL_PAGE_SIZE).describe(`Number of object IDs to return per page (max ${MAX_SEARCH_PAGE_SIZE})`), }); export const SearchInputSchema = SearchInputBaseSchema.refine( ({ dateBegin, dateEnd }) => (dateBegin === undefined) === (dateEnd === undefined), { message: 'dateBegin and dateEnd must both be provided when filtering by date range.', path: ['dateBegin'], }, ); - src/types/types.ts:122-128 (schema)SearchMuseumObjectsStructuredContentSchema defines the structured output shape: total, page, pageSize, totalPages, and objectIDs array.
export const SearchMuseumObjectsStructuredContentSchema = z.object({ total: z.number().describe('Total number of matching objects across all pages'), page: z.number().int().positive().describe('Current 1-based page number returned by the tool'), pageSize: z.number().int().positive().describe('Number of object IDs returned per page'), totalPages: z.number().int().nonnegative().describe('Total number of pages available for this query'), objectIDs: z.array(z.number()).describe('Paginated list of object IDs for the current page'), }); - src/MetMuseumServer.ts:115-136 (registration)Registration of the 'search-museum-objects' tool via registerAppTool, binding search.inputSchema, SearchMuseumObjectsStructuredContentSchema, and search.execute.
registerAppTool( server, search.name, { description: search.description, inputSchema: search.inputSchema.shape, outputSchema: SearchMuseumObjectsStructuredContentSchema.shape, annotations: { title: 'Search Met Museum Objects', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, _meta: { ui: { visibility: ['model', 'app'], }, }, }, search.execute.bind(search), ); - The searchObjects method on MetMuseumApiClient that builds the search URL with query parameters and calls the Met Museum Collection API's /search endpoint.
public async searchObjects({ q, hasImages, title, departmentId, isHighlight, tags, isOnView, artistOrCulture, medium, geoLocation, dateBegin, dateEnd, }: { q: string; hasImages?: boolean; title?: boolean; departmentId?: number; isHighlight?: boolean; tags?: boolean; isOnView?: boolean; artistOrCulture?: boolean; medium?: string; geoLocation?: string; dateBegin?: number; dateEnd?: number; }): Promise<z.infer<typeof SearchResponseSchema>> { if ((dateBegin === undefined) !== (dateEnd === undefined)) { throw new MetMuseumApiError( 'Both dateBegin and dateEnd are required when filtering by date range.', undefined, true, ); } const url = new URL(this.searchUrl); url.searchParams.set('q', q); if (hasImages) { url.searchParams.set('hasImages', 'true'); } if (title) { url.searchParams.set('title', 'true'); } if (typeof departmentId === 'number') { url.searchParams.set('departmentId', departmentId.toString()); } if (isHighlight) { url.searchParams.set('isHighlight', 'true'); } if (tags) { url.searchParams.set('tags', 'true'); } if (isOnView) { url.searchParams.set('isOnView', 'true'); } if (artistOrCulture) { url.searchParams.set('artistOrCulture', 'true'); } if (medium) { url.searchParams.set('medium', medium); } if (geoLocation) { url.searchParams.set('geoLocation', geoLocation); } if (typeof dateBegin === 'number') { url.searchParams.set('dateBegin', dateBegin.toString()); } if (typeof dateEnd === 'number') { url.searchParams.set('dateEnd', dateEnd.toString()); } return await this.fetchAndParse(url.toString(), SearchResponseSchema, 'search'); }