Skip to main content
Glama
mikechao

Met Museum MCP Server

search-museum-objects

Read-onlyIdempotent

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

TableJSON Schema
NameRequiredDescriptionDefault
qYesThe search query, Returns a listing of all Object IDs for objects that contain the search query within the object's data
hasImagesNoOnly returns objects that have images
titleNoThis should be set to true if you want to search for objects by title
isHighlightNoOnly returns objects designated as highlights
tagsNoOnly returns objects that have subject keyword tags
isOnViewNoOnly returns objects currently on view
artistOrCultureNoWhen true, q is matched against artist or culture
departmentIdNoReturns objects that are in the specified department. The departmentId should come from the 'list-departments' tool.
mediumNoRestricts search to objects with the specified medium
geoLocationNoRestricts search to objects with the specified geographic location
dateBeginNoStart year for a date range filter. Must be provided together with dateEnd.
dateEndNoEnd year for a date range filter. Must be provided together with dateBegin.
pageNo1-based page number for paginated object IDs
pageSizeNoNumber of object IDs to return per page (max 100)

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
totalYesTotal number of matching objects across all pages
pageYesCurrent 1-based page number returned by the tool
pageSizeYesNumber of object IDs returned per page
totalPagesYesTotal number of pages available for this query
objectIDsYesPaginated 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'],
      },
    );
  • 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'),
    });
  • 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');
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate read-only, non-destructive, idempotent behavior. The description adds value by detailing the exact output (total objects found + paginated object IDs) and clarifying default behaviors (e.g., hasImages defaults to false). No contradictions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Six sentences, front-loaded with purpose and return format, logically organized. No fluff; every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (14 params, 1 required, output schema exists), the description is complete. It covers main output, alternative usage, parameter details, and cross-tool dependency. The existing output schema complements it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema already covers 100% parameters with descriptions and defaults. The description summarizes and adds a cross-reference (departmentId from list-departments), but does not significantly enhance understanding beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the purpose: 'Search for objects in the Metropolitan Museum of Art (Met Museum).' It also mentions return type (total count and paginated object IDs). However, it does not explicitly differentiate from sibling tools like get-museum-object, though the name and context imply it.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to avoid this tool ('If the Met Explorer app is open... prefer using those results). It also references the list-departments tool for a parameter. However, it does not compare with other search alternatives or mention get-museum-object.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mikechao/metmuseum-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server