Rijksmuseum MCP Server

import axios, { AxiosInstance, AxiosError } from 'axios'; import { ArtworkSearchResult, ArtworkDetails, ImageTiles, UserSet, TimelineArtwork, SearchArtworkArguments, GetUserSetsArguments, UserSetsResponse, GetUserSetDetailsArguments, UserSetDetails } from '../types.js'; export class RijksmuseumApiClient { private axiosInstance: AxiosInstance; private readonly BASE_URL = 'https://www.rijksmuseum.nl/api'; private readonly ENDPOINTS = { COLLECTION: 'collection' }; constructor(apiKey: string) { if (!apiKey) { throw new Error('API key is required for Rijksmuseum API'); } this.axiosInstance = axios.create({ baseURL: this.BASE_URL, params: { key: apiKey, format: 'json' } }); // Add response interceptor for error handling this.axiosInstance.interceptors.response.use( response => response, (error: AxiosError) => { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx throw new Error(`Rijksmuseum API error: ${error.response.status} - ${error.response.statusText}`); } else if (error.request) { // The request was made but no response was received throw new Error('No response received from Rijksmuseum API'); } else { // Something happened in setting up the request that triggered an Error throw new Error(`Error making request to Rijksmuseum API: ${error.message}`); } } ); } async searchArtworks(params: SearchArtworkArguments): Promise<ArtworkSearchResult[]> { try { // Validate page and pageSize constraints const p = params.p ?? 0; const ps = params.ps ?? 10; if (p * ps > 10000) { throw new Error('Page * pageSize cannot exceed 10,000'); } // Build API parameters const apiParams: Record<string, any> = { p, ps }; // Add optional parameters if they exist if (params.q) apiParams.q = params.q; if (params.involvedMaker) apiParams.involvedMaker = encodeURIComponent(params.involvedMaker); if (params.type) apiParams.type = encodeURIComponent(params.type); if (params.material) apiParams.material = encodeURIComponent(params.material); if (params.technique) apiParams.technique = encodeURIComponent(params.technique); if (params.century) apiParams['f.dating.period'] = params.century; if (params.color) apiParams['f.normalized32Colors.hex'] = params.color.replace('#', ''); if (params.imgonly !== undefined) apiParams.imgonly = params.imgonly; if (params.toppieces !== undefined) apiParams.toppieces = params.toppieces; if (params.sortBy) apiParams.s = params.sortBy; const culture = params.culture ?? 'en'; const response = await this.axiosInstance.get(`${culture}/${this.ENDPOINTS.COLLECTION}`, { params: apiParams }); if (!response.data.artObjects) { throw new Error('Invalid response from Rijksmuseum API: missing artObjects'); } return response.data.artObjects; } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while searching artworks'); } } async getArtworkDetails(objectNumber: string, culture: 'nl' | 'en' = 'en'): Promise<ArtworkDetails> { try { if (!objectNumber) { throw new Error('Object number is required'); } // Ensure object number is properly encoded const encodedObjectNumber = encodeURIComponent(objectNumber); const response = await this.axiosInstance.get(`${culture}/${this.ENDPOINTS.COLLECTION}/${encodedObjectNumber}`); if (!response.data.artObject) { throw new Error('Invalid response from Rijksmuseum API: missing artObject'); } return response.data; } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching artwork details'); } } async getArtworkImageTiles(objectNumber: string, culture: 'nl' | 'en' = 'en'): Promise<ImageTiles> { try { if (!objectNumber) { throw new Error('Object number is required'); } const encodedObjectNumber = encodeURIComponent(objectNumber); const response = await this.axiosInstance.get(`${culture}/${this.ENDPOINTS.COLLECTION}/${encodedObjectNumber}/tiles`); if (!response.data.levels || !Array.isArray(response.data.levels)) { throw new Error('Invalid response from Rijksmuseum API: missing or invalid image tiles data'); } // Validate the structure of each level response.data.levels.forEach((level: any, index: number) => { if (!level.name || typeof level.width !== 'number' || typeof level.height !== 'number' || !Array.isArray(level.tiles)) { throw new Error(`Invalid level data at index ${index}`); } }); return response.data; } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching artwork image tiles'); } } async getUserSets({ page = 0, pageSize = 10, culture = 'en' }: GetUserSetsArguments = {}): Promise<UserSetsResponse> { try { // Validate pagination constraints if (page * pageSize > 10000) { throw new Error('Page * pageSize cannot exceed 10,000'); } if (pageSize < 1 || pageSize > 100) { throw new Error('Page size must be between 1 and 100'); } const response = await this.axiosInstance.get(`${culture}/usersets`, { params: { page, pageSize } }); if (!response.data.userSets || !Array.isArray(response.data.userSets)) { throw new Error('Invalid response from Rijksmuseum API: missing or invalid userSets data'); } return response.data; } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching user sets'); } } async getUserSetDetails({ setId, culture = 'en', page = 0, pageSize = 25 }: GetUserSetDetailsArguments): Promise<UserSetDetails> { try { if (!setId) { throw new Error('Set ID is required'); } // Validate pagination constraints if (page * pageSize > 10000) { throw new Error('Page * pageSize cannot exceed 10,000'); } if (pageSize < 1 || pageSize > 100) { throw new Error('Page size must be between 1 and 100'); } const encodedSetId = encodeURIComponent(setId); const response = await this.axiosInstance.get(`${culture}/usersets/${encodedSetId}`, { params: { page, pageSize } }); if (!response.data.userSet) { throw new Error('Invalid response from Rijksmuseum API: missing user set data'); } // Validate the structure of setItems if they exist if (response.data.userSet.setItems && !Array.isArray(response.data.userSet.setItems)) { throw new Error('Invalid response from Rijksmuseum API: invalid setItems format'); } return response.data; } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching user set details'); } } async getArtistTimeline(artist: string, maxWorks: number = 10): Promise<TimelineArtwork[]> { try { if (!artist) { throw new Error('Artist name is required'); } const response = await this.axiosInstance.get(`${this.ENDPOINTS.COLLECTION}`, { params: { involvedMaker: encodeURIComponent(artist), ps: maxWorks, s: 'chronologic', imgonly: true } }); if (!response.data.artObjects) { throw new Error('Invalid response from Rijksmuseum API: missing artObjects'); } return response.data.artObjects.map((artwork: ArtworkSearchResult) => ({ year: artwork.longTitle.match(/\d{4}/)?.[0] || "Unknown", title: artwork.title, objectNumber: artwork.objectNumber, description: artwork.longTitle, image: artwork.webImage ? artwork.webImage.url : null })); } catch (error) { if (error instanceof Error) { throw error; } throw new Error('Unknown error occurred while fetching artist timeline'); } } }