UNPKG

discogs-mcp-server

Version:
1,623 lines (1,609 loc) 106 kB
#!/usr/bin/env node import { FastMCP, imageContent, UserError } from 'fastmcp'; import dotenv from 'dotenv'; import { z } from 'zod'; // src/version.ts var VERSION = "0.4.2"; // src/config.ts dotenv.config(); var config = { discogs: { apiUrl: process.env.DISCOGS_API_URL || "https://api.discogs.com", /* Some MCP clients can't handle large amounts of data. * The client may explicitly request more at their own peril. */ defaultPerPage: 5, mediaType: process.env.DISCOGS_MEDIA_TYPE || "application/vnd.discogs.v2.discogs+json", personalAccessToken: process.env.DISCOGS_PERSONAL_ACCESS_TOKEN, userAgent: process.env.DISCOGS_USER_AGENT || `DiscogsMCPServer/${VERSION}` }, server: { name: process.env.SERVER_NAME || "Discogs MCP Server", port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3001 } }; function validateConfig() { const missingVars = []; if (!process.env.DISCOGS_PERSONAL_ACCESS_TOKEN) { missingVars.push("DISCOGS_PERSONAL_ACCESS_TOKEN"); } if (missingVars.length > 0) { throw new Error(`Missing required environment variables: ${missingVars.join(", ")}`); } } var DiscogsError = class extends Error { constructor(message, status, response) { super(message); this.status = status; this.response = response; this.name = new.target.name; } }; var DiscogsAuthenticationError = class extends DiscogsError { constructor(message = "Authentication failed") { super(message, 401, { message }); this.name = new.target.name; } }; var DiscogsMethodNotAllowedError = class extends DiscogsError { constructor(message = "Method not allowed") { super(message, 405, { message }); this.name = new.target.name; } }; var DiscogsPermissionError = class extends DiscogsError { constructor(message = "Insufficient permissions") { super(message, 403, { message }); this.name = new.target.name; } }; var DiscogsRateLimitError = class extends DiscogsError { constructor(message = "Rate limit exceeded", resetAt) { super(message, 429, { message, reset_at: resetAt.toISOString() }); this.resetAt = resetAt; this.name = new.target.name; } }; var DiscogsResourceNotFoundError = class extends DiscogsError { constructor(message = "Resource not found") { super(message, 404, { message }); this.name = new.target.name; } }; var DiscogsValidationFailedError = class extends DiscogsError { constructor(response) { let message = "Validation failed"; if (response && typeof response === "object" && response !== null) { const detail = response.detail; if (Array.isArray(detail) && detail.length > 0 && detail[0].msg) { message = detail[0].msg; } } super(message, 422, { message }); this.name = new.target.name; } }; function createDiscogsError(status, response) { switch (status) { case 401: return new DiscogsAuthenticationError(response?.message); case 403: return new DiscogsPermissionError(response?.message); case 404: return new DiscogsResourceNotFoundError(response?.message || "Resource"); case 405: return new DiscogsMethodNotAllowedError(response?.message); case 422: return new DiscogsValidationFailedError(response); case 429: return new DiscogsRateLimitError( response?.message, new Date(response?.reset_at || Date.now() + 6e4) ); default: return new DiscogsError(response?.message || "Discogs API error", status, response); } } function formatDiscogsError(error) { let message; if (error instanceof Error) { message = error.message; } else { message = String(error); } return new UserError(message); } function isDiscogsError(error) { return error instanceof DiscogsError; } var log = { _log: (level, ...args) => { const msg = `[${level} ${(/* @__PURE__ */ new Date()).toISOString()}] ${args.join(" ")} `; process.stderr.write(msg); }, info: (...args) => { log._log("INFO", ...args); }, debug: (...args) => { log._log("DEBUG", ...args); }, warn: (...args) => { log._log("WARN", ...args); }, error: (...args) => { log._log("ERROR", ...args); } }; var urlOrEmptySchema = () => { return z.string().refine((val) => val === "" || /^https?:\/\/.+/.test(val), { message: "Must be a valid URL or empty string" }); }; var CurrencyCodeSchema = z.enum([ "USD", // US Dollar "GBP", // British Pound "EUR", // Euro "CAD", // Canadian Dollar "AUD", // Australian Dollar "JPY", // Japanese Yen "CHF", // Swiss Franc "MXN", // Mexican Peso "BRL", // Brazilian Real "NZD", // New Zealand Dollar "SEK", // Swedish Krona "ZAR" // South African Rand ]); var ImageSchema = z.object({ width: z.number().int().optional(), height: z.number().int().optional(), resource_url: urlOrEmptySchema(), type: z.string().optional(), uri: urlOrEmptySchema(), uri150: urlOrEmptySchema().optional() }); var FilteredResponseSchema = z.object({ filters: z.object({ applied: z.record(z.string(), z.array(z.any())).default({}), available: z.record(z.string(), z.record(z.string(), z.number().int())).default({}) }), filter_facets: z.array( z.object({ title: z.string(), id: z.string(), values: z.array( z.object({ title: z.string(), value: z.string(), count: z.number().int() }) ), allows_multiple_values: z.boolean() }) ) }); var PaginationSchema = z.object({ page: z.number().int().min(0).optional(), per_page: z.number().int().min(0).optional(), pages: z.number().int().min(0), items: z.number().int().min(0), urls: z.object({ first: z.string().url().optional(), prev: z.string().url().optional(), next: z.string().url().optional(), last: z.string().url().optional() }).optional() }); var PaginatedResponseSchema = (itemSchema, resultsFieldName) => z.object({ pagination: PaginationSchema, [resultsFieldName]: z.array(itemSchema) }); var PaginatedResponseWithObjectSchema = (itemSchema, resultsFieldName) => z.object({ pagination: PaginationSchema, [resultsFieldName]: itemSchema }); var QueryParamsSchema = (validSortKeys = []) => z.object({ // Pagination page: z.number().int().min(1).optional(), per_page: z.number().int().min(1).max(100).optional(), // Sorting sort: z.enum(validSortKeys).optional(), sort_order: z.enum(["asc", "desc"]).optional() }); var StatusSchema = z.enum(["Accepted", "Draft", "Deleted", "Rejected"]); var UsernameInputSchema = z.object({ username: z.string().min(1, "username is required") }); // src/types/artist.ts var ArtistIdParamSchema = z.object({ artist_id: z.number() }); var ArtistBasicSchema = z.object({ id: z.number(), anv: z.string(), join: z.string(), name: z.string(), resource_url: urlOrEmptySchema(), role: z.string(), tracks: z.string() }); var ArtistSchema = z.object({ id: z.number(), aliases: z.array( z.object({ id: z.number(), name: z.string(), resource_url: urlOrEmptySchema(), thumbnail_url: urlOrEmptySchema().optional() }) ).optional(), data_quality: z.string().optional(), images: z.array(ImageSchema).optional(), members: z.array( z.object({ id: z.number(), active: z.boolean().optional(), name: z.string(), resource_url: urlOrEmptySchema(), thumbnail_url: urlOrEmptySchema().optional() }) ).optional(), name: z.string(), namevariations: z.array(z.string()).optional(), profile: z.string().optional(), realname: z.string().optional(), releases_url: urlOrEmptySchema().optional(), resource_url: urlOrEmptySchema(), uri: urlOrEmptySchema().optional(), urls: z.array(z.string()).optional() }); var ArtistReleaseSchema = z.object({ id: z.number(), artist: z.string(), catno: z.string().optional(), format: z.string().optional(), label: z.string().optional(), main_release: z.number().optional(), resource_url: urlOrEmptySchema(), role: z.string().optional(), status: z.string().optional(), stats: z.object({ community: z.object({ in_collection: z.number(), in_wantlist: z.number() }), user: z.object({ in_collection: z.number(), in_wantlist: z.number() }) }).optional(), thumb: urlOrEmptySchema().optional(), title: z.string(), trackinfo: z.string().optional(), type: z.string().optional(), year: z.number().optional() }); var ArtistReleasesParamsSchema = ArtistIdParamSchema.merge( QueryParamsSchema(["year", "title", "format"]) ); var ArtistReleasesSchema = PaginatedResponseSchema(ArtistReleaseSchema, "releases"); // src/services/index.ts var DiscogsService = class { constructor(servicePath) { this.servicePath = servicePath; if (!config.discogs.personalAccessToken || !config.discogs.userAgent) { throw new Error("Discogs API configuration is incomplete"); } this.baseUrl = `${config.discogs.apiUrl}${servicePath}`; this.headers = { Accept: config.discogs.mediaType, Authorization: `Discogs token=${config.discogs.personalAccessToken}`, "Content-Type": "application/json", "User-Agent": config.discogs.userAgent }; } baseUrl; headers; async request(path, options) { const url = new URL(`${this.baseUrl}${path}`); if (options?.params) { Object.entries(options.params).forEach(([key, value]) => { if (value !== void 0) { url.searchParams.append(key, String(value)); } }); } if (!url.searchParams.has("per_page")) { url.searchParams.append("per_page", String(config.discogs.defaultPerPage)); } const response = await fetch(url.toString(), { method: options?.method || "GET", headers: this.headers, body: options?.body ? JSON.stringify(options.body) : void 0 }); const contentType = response.headers.get("content-type"); const isJson = contentType && contentType.includes("application/json"); let responseBody; try { responseBody = isJson ? await response.json() : await response.text(); } catch { responseBody = { message: "Failed to parse response" }; } if (!response.ok) { throw createDiscogsError(response.status, responseBody); } return responseBody; } }; var BaseUserService = class extends DiscogsService { constructor() { super("/users"); } }; // src/services/artist.ts var ArtistService = class extends DiscogsService { constructor() { super("/artists"); } /** * Get an artist * * @param params - Parameters containing the artist ID * @returns {Artist} The artist information * @throws {DiscogsResourceNotFoundError} If the artist cannot be found * @throws {Error} If there's an unexpected error */ async get({ artist_id }) { try { const response = await this.request(`/${artist_id}`); const validatedResponse = ArtistSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get artist: ${String(error)}`); } } /** * Get an artist's releases * * @param params - Parameters containing the artist ID and pagination options * @returns {ArtistReleases} The artist releases * @throws {DiscogsResourceNotFoundError} If the artist cannot be found * @throws {Error} If there's an unexpected error */ async getReleases({ artist_id, ...options }) { try { const response = await this.request(`/${artist_id}/releases`, { params: options }); const validatedResponse = ArtistReleasesSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get artist releases: ${String(error)}`); } } }; var SearchParamsSchema = z.object({ q: z.string().optional(), type: z.enum(["artist", "label", "master", "release"]).optional(), title: z.string().optional(), release_title: z.string().optional(), credit: z.string().optional(), artist: z.string().optional(), anv: z.string().optional(), label: z.string().optional(), genre: z.string().optional(), style: z.string().optional(), country: z.string().optional(), year: z.string().optional(), format: z.string().optional(), catno: z.string().optional(), barcode: z.string().optional(), track: z.string().optional(), submitter: z.string().optional(), contributor: z.string().optional() }).merge(QueryParamsSchema(["title", "artist", "year"])); var SearchResultSchema = z.object({ id: z.number(), barcode: z.array(z.string()).optional(), catno: z.string().optional(), community: z.object({ have: z.number(), want: z.number() }).optional(), country: z.string().optional(), cover_image: urlOrEmptySchema().optional(), format: z.array(z.string()).optional(), format_quantity: z.number().optional(), formats: z.array( z.object({ descriptions: z.array(z.string()).optional(), name: z.string(), qty: z.string(), text: z.string().optional() }) ).optional(), genre: z.array(z.string()).optional(), label: z.array(z.string()).optional(), master_id: z.number().nullable().optional(), master_url: urlOrEmptySchema().nullable().optional(), resource_url: urlOrEmptySchema(), style: z.array(z.string()).optional(), thumb: urlOrEmptySchema().optional(), title: z.string(), type: z.enum(["artist", "label", "master", "release"]), uri: z.string(), user_data: z.object({ in_collection: z.boolean(), in_wantlist: z.boolean() }).optional(), year: z.string().optional() }); var SearchResultsSchema = PaginatedResponseSchema(SearchResultSchema, "results"); // src/services/database.ts var DatabaseService = class extends DiscogsService { constructor() { super("/database"); } /** * Issue a search query to the Discogs database * * @param params - Search parameters * @throws {DiscogsAuthenticationError} If authentication fails * @throws {Error} If the search times out or an unexpected error occurs * @returns {SearchResults} Search results */ async search(params) { try { const response = await this.request("/search", { params }); const validatedResponse = SearchResultsSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to search database: ${String(error)}`); } } }; var LabelIdParamSchema = z.object({ label_id: z.number() }); var LabelBasicSchema = z.object({ id: z.number(), catno: z.string(), entity_type: z.string().optional(), entity_type_name: z.string().optional(), name: z.string(), resource_url: urlOrEmptySchema() }); var LabelSchema = z.object({ id: z.number(), contact_info: z.string().optional(), data_quality: z.string().optional(), images: z.array(ImageSchema).optional(), name: z.string(), parent_label: z.object({ id: z.number(), name: z.string(), resource_url: urlOrEmptySchema() }).optional(), profile: z.string().optional(), releases_url: urlOrEmptySchema().optional(), resource_url: urlOrEmptySchema(), sublabels: z.array( z.object({ id: z.number(), name: z.string(), resource_url: urlOrEmptySchema() }) ).optional(), uri: urlOrEmptySchema().optional(), urls: z.array(z.string()).optional() }); var LabelReleasesParamsSchema = LabelIdParamSchema.merge(QueryParamsSchema()); var LabelReleasesSchema = PaginatedResponseSchema(ArtistReleaseSchema, "releases"); // src/services/label.ts var LabelService = class extends DiscogsService { constructor() { super("/labels"); } /** * Get a label * * @param params - Parameters containing the label ID * @returns {Label} The label information * @throws {DiscogsResourceNotFoundError} If the label cannot be found * @throws {Error} If there's an unexpected error */ async get({ label_id }) { try { const response = await this.request(`/${label_id}`); const validatedResponse = LabelSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get label: ${String(error)}`); } } /** * Returns a list of Releases associated with the label * * @param params - Parameters containing the label ID and pagination options * @returns {LabelReleases} The label releases * @throws {DiscogsResourceNotFoundError} If the label cannot be found * @throws {Error} If there's an unexpected error */ async getReleases({ label_id, ...params }) { try { const response = await this.request(`/${label_id}/releases`, { params }); const validatedResponse = LabelReleasesSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get label releases: ${String(error)}`); } } }; var ReleaseFormatSchema = z.object({ descriptions: z.array(z.string()).optional(), name: z.string(), qty: z.string(), text: z.string().optional() }); var BasicInformationSchema = z.object({ id: z.number(), artists: z.array(ArtistBasicSchema), cover_image: urlOrEmptySchema(), formats: z.array(ReleaseFormatSchema), genres: z.array(z.string()).optional(), master_id: z.number().optional(), master_url: urlOrEmptySchema().nullable().optional(), labels: z.array(LabelBasicSchema), resource_url: urlOrEmptySchema(), styles: z.array(z.string()).optional(), thumb: urlOrEmptySchema(), title: z.string(), year: z.number() }); var ReleaseSchema = z.object({ id: z.number().int(), artists_sort: z.string().optional(), artists: z.array(ArtistBasicSchema), blocked_from_sale: z.boolean().optional(), companies: z.array( z.object({ id: z.number().int().optional(), catno: z.string().optional(), entity_type: z.string().optional(), entity_type_name: z.string().optional(), name: z.string().optional(), resource_url: urlOrEmptySchema().optional(), thumbnail_url: urlOrEmptySchema().optional() }) ).optional(), community: z.object({ contributors: z.array( z.object({ resource_url: urlOrEmptySchema().optional(), username: z.string().optional() }) ).optional(), data_quality: z.string().optional(), have: z.number().int().optional(), rating: z.object({ average: z.number().optional(), count: z.number().int().optional() }).optional(), status: z.string().optional(), submitter: z.object({ resource_url: urlOrEmptySchema().optional(), username: z.string().optional() }).optional(), want: z.number().int().optional() }).optional(), country: z.string().optional(), data_quality: z.string().optional(), date_added: z.string().optional(), date_changed: z.string().optional(), estimated_weight: z.number().int().optional(), extraartists: z.array(ArtistBasicSchema).optional(), format_quantity: z.number().int().optional(), formats: z.array(ReleaseFormatSchema).optional(), genres: z.array(z.string()).optional(), identifiers: z.array( z.object({ type: z.string(), value: z.string(), description: z.string().optional() }) ).optional(), images: z.array(ImageSchema).optional(), labels: z.array(LabelBasicSchema).optional(), lowest_price: z.number().nullable().optional(), master_id: z.number().optional(), master_url: urlOrEmptySchema().optional(), notes: z.string().optional(), num_for_sale: z.number().int().optional(), released: z.string().optional(), released_formatted: z.string().optional(), resource_url: urlOrEmptySchema(), series: z.array(z.unknown()).optional(), status: z.string().optional(), styles: z.array(z.string()).optional(), thumb: urlOrEmptySchema().optional(), title: z.string(), tracklist: z.array( z.object({ duration: z.string().optional(), position: z.string(), title: z.string(), type_: z.string().optional(), extraartists: z.array(ArtistBasicSchema).optional() }) ).optional(), uri: urlOrEmptySchema().optional(), videos: z.array( z.object({ description: z.string().nullable().optional(), duration: z.number().int().optional(), embed: z.boolean().optional(), title: z.string().optional(), uri: urlOrEmptySchema().optional() }) ).optional(), year: z.number() }); var ReleaseIdParamSchema = z.object({ release_id: z.number().min(1, "The release_id must be non-zero") }); var ReleaseParamsSchema = ReleaseIdParamSchema.extend({ curr_abbr: CurrencyCodeSchema.optional() }); var ReleaseRatingSchema = UsernameInputSchema.merge(ReleaseIdParamSchema).extend({ rating: z.number() }); var ReleaseRatingCommunitySchema = ReleaseIdParamSchema.extend({ rating: z.object({ average: z.number(), count: z.number().int() }) }); var ReleaseRatingParamsSchema = UsernameInputSchema.merge(ReleaseIdParamSchema); var ReleaseRatingEditParamsSchema = ReleaseRatingParamsSchema.extend({ rating: z.number().int().min(1, "The rating must be at least 1").max(5, "The rating must be at most 5") }); // src/types/master.ts var MasterReleaseIdParamSchema = z.object({ master_id: z.number().int() }); var MasterReleaseVersionsParamSchema = MasterReleaseIdParamSchema.extend({ format: z.string().optional(), label: z.string().optional(), released: z.string().optional(), country: z.string().optional() }).merge(QueryParamsSchema(["released", "title", "format", "label", "catno", "country"])); var MasterReleaseSchema = ReleaseSchema.extend({ main_release: z.number(), most_recent_release: z.number(), versions_url: urlOrEmptySchema(), main_release_url: urlOrEmptySchema(), most_recent_release_url: urlOrEmptySchema(), tracklist: z.array( z.object({ position: z.string(), type_: z.string().optional(), title: z.string(), duration: z.string().optional(), extraartists: z.array(ArtistBasicSchema).optional() }) ).optional(), artists: z.array( ArtistBasicSchema.extend({ thumbnail_url: urlOrEmptySchema().optional() }) ) }); var MasterReleaseVersionItemSchema = z.object({ id: z.number().int(), label: z.string(), country: z.string(), title: z.string(), major_formats: z.array(z.string()), format: z.string(), catno: z.string(), released: z.string(), status: StatusSchema, resource_url: urlOrEmptySchema(), thumb: urlOrEmptySchema().optional(), stats: z.object({ community: z.object({ in_wantlist: z.number().int(), in_collection: z.number().int() }).optional(), user: z.object({ in_wantlist: z.number().int(), in_collection: z.number().int() }).optional() }) }); var MasterReleaseVersionsResponseSchema = z.object({ ...PaginatedResponseSchema(MasterReleaseVersionItemSchema, "versions").shape, ...FilteredResponseSchema.shape }); // src/services/master.ts var MasterReleaseService = class extends DiscogsService { constructor() { super("/masters"); } /** * Get a master release * * @param params - Parameters containing the master release ID * @returns {MasterRelease} The master release information * @throws {DiscogsResourceNotFoundError} If the master release cannot be found * @throws {Error} If there's an unexpected error */ async get({ master_id }) { try { const response = await this.request(`/${master_id}`); const validatedResponse = MasterReleaseSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get master release: ${String(error)}`); } } /** * Retrieves a list of all Releases that are versions of this master * * @param params - Parameters containing the master release ID and optional query parameters * @returns {MasterReleaseVersionsResponse} The master release versions information * @throws {DiscogsResourceNotFoundError} If the master release versions cannot be found * @throws {Error} If there's an unexpected error */ async getVersions({ master_id, ...options }) { try { const response = await this.request(`/${master_id}/versions`, { params: options }); const validatedResponse = MasterReleaseVersionsResponseSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get master release versions: ${String(error)}`); } } }; // src/services/release.ts var ReleaseService = class extends DiscogsService { constructor() { super("/releases"); } /** * Deletes the release's rating for a given user * * @param params - Parameters for the request * @throws {DiscogsAuthenticationError} If authentication fails * @throws {DiscogsPermissionError} If trying to delete a release rating of another user * @throws {DiscogsResourceNotFoundError} If the release or user cannot be found * @throws {Error} If there's an unexpected error */ async deleteRatingByUser({ username, release_id }) { try { await this.request(`/${release_id}/rating/${username}`, { method: "DELETE" }); } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to delete release rating: ${String(error)}`); } } /** * Updates the release's rating for a given user * * @param params - Parameters for the request * @returns {ReleaseRating} The updated release rating * @throws {DiscogsAuthenticationError} If authentication fails * @throws {DiscogsPermissionError} If trying to edit a release rating of another user * @throws {DiscogsResourceNotFoundError} If the release or user cannot be found * @throws {Error} If there's an unexpected error */ async editRatingByUser({ username, release_id, rating }) { try { const response = await this.request(`/${release_id}/rating/${username}`, { method: "PUT", body: { rating } }); const validatedResponse = ReleaseRatingSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to edit release rating: ${String(error)}`); } } /** * Get a release * * @param params - Parameters for the request * @returns {Release} The release information * @throws {DiscogsResourceNotFoundError} If the release cannot be found * @throws {Error} If there's an unexpected error */ async get({ release_id, ...options }) { try { const response = await this.request(`/${release_id}`, { params: options }); const validatedResponse = ReleaseSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get release: ${String(error)}`); } } /** * Retrieves the community release rating average and count * * @param params - Parameters for the request * @returns {ReleaseRatingCommunity} The release community rating * @throws {DiscogsResourceNotFoundError} If the release cannot be found * @throws {Error} If there's an unexpected error */ async getCommunityRating({ release_id }) { try { const response = await this.request(`/${release_id}/rating`); const validatedResponse = ReleaseRatingCommunitySchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get release community rating: ${String(error)}`); } } /** * Retrieves the release's rating for a given user * * @param params - Parameters for the request * @returns {ReleaseRating} The release rating * @throws {DiscogsResourceNotFoundError} If the release or user cannot be found * @throws {Error} If there's an unexpected error */ async getRatingByUser({ username, release_id }) { try { const response = await this.request(`/${release_id}/rating/${username}`); const validatedResponse = ReleaseRatingSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get release rating: ${String(error)}`); } } }; // src/tools/database.ts var deleteReleaseRatingTool = { name: "delete_release_rating", description: `Deletes the release's rating for a given user`, parameters: ReleaseRatingParamsSchema, execute: async (args) => { try { const releaseService = new ReleaseService(); await releaseService.deleteRatingByUser(args); return "Release rating deleted successfully"; } catch (error) { throw formatDiscogsError(error); } } }; var editReleaseRatingTool = { name: "edit_release_rating", description: `Updates the release's rating for a given user`, parameters: ReleaseRatingEditParamsSchema, execute: async (args) => { try { const releaseService = new ReleaseService(); const releaseRating = await releaseService.editRatingByUser(args); return JSON.stringify(releaseRating); } catch (error) { throw formatDiscogsError(error); } } }; var getArtistTool = { name: "get_artist", description: "Get an artist", parameters: ArtistIdParamSchema, execute: async (args) => { try { const artistService = new ArtistService(); const artist = await artistService.get(args); return JSON.stringify(artist); } catch (error) { throw formatDiscogsError(error); } } }; var getArtistReleasesTool = { name: "get_artist_releases", description: `Get an artist's releases`, parameters: ArtistReleasesParamsSchema, execute: async (args) => { try { const artistService = new ArtistService(); const artistReleases = await artistService.getReleases(args); return JSON.stringify(artistReleases); } catch (error) { throw formatDiscogsError(error); } } }; var getLabelTool = { name: "get_label", description: "Get a label", parameters: LabelIdParamSchema, execute: async (args) => { try { const labelService = new LabelService(); const label = await labelService.get(args); return JSON.stringify(label); } catch (error) { throw formatDiscogsError(error); } } }; var getLabelReleasesTool = { name: "get_label_releases", description: "Returns a list of Releases associated with the label", parameters: LabelReleasesParamsSchema, execute: async (args) => { try { const labelService = new LabelService(); const labelReleases = await labelService.getReleases(args); return JSON.stringify(labelReleases); } catch (error) { throw formatDiscogsError(error); } } }; var getMasterReleaseTool = { name: "get_master_release", description: "Get a master release", parameters: MasterReleaseIdParamSchema, execute: async (args) => { try { const masterReleaseService = new MasterReleaseService(); const masterRelease = await masterReleaseService.get(args); return JSON.stringify(masterRelease); } catch (error) { throw formatDiscogsError(error); } } }; var getMasterReleaseVersionsTool = { name: "get_master_release_versions", description: "Retrieves a list of all Releases that are versions of this master", parameters: MasterReleaseVersionsParamSchema, execute: async (args) => { try { const masterReleaseService = new MasterReleaseService(); const masterReleaseVersions = await masterReleaseService.getVersions(args); return JSON.stringify(masterReleaseVersions); } catch (error) { throw formatDiscogsError(error); } } }; var getReleaseTool = { name: "get_release", description: "Get a release", parameters: ReleaseParamsSchema, execute: async (args) => { try { const releaseService = new ReleaseService(); const release = await releaseService.get(args); return JSON.stringify(release); } catch (error) { throw formatDiscogsError(error); } } }; var getReleaseCommunityRatingTool = { name: "get_release_community_rating", description: "Retrieves the release community rating average and count", parameters: ReleaseIdParamSchema, execute: async (args) => { try { const releaseService = new ReleaseService(); const releaseRating = await releaseService.getCommunityRating(args); return JSON.stringify(releaseRating); } catch (error) { throw formatDiscogsError(error); } } }; var getReleaseRatingTool = { name: "get_release_rating_by_user", description: `Retrieves the release's rating for a given user`, parameters: ReleaseRatingParamsSchema, execute: async (args) => { try { const releaseService = new ReleaseService(); const releaseRating = await releaseService.getRatingByUser(args); return JSON.stringify(releaseRating); } catch (error) { throw formatDiscogsError(error); } } }; var searchTool = { name: "search", description: "Issue a search query to the Discogs database", parameters: SearchParamsSchema, execute: async (args) => { try { const databaseService = new DatabaseService(); const searchResults = await databaseService.search(args); return JSON.stringify(searchResults); } catch (error) { throw formatDiscogsError(error); } } }; function registerDatabaseTools(server) { server.addTool(getReleaseTool); server.addTool(getReleaseRatingTool); server.addTool(editReleaseRatingTool); server.addTool(deleteReleaseRatingTool); server.addTool(getReleaseCommunityRatingTool); server.addTool(getMasterReleaseTool); server.addTool(getMasterReleaseVersionsTool); server.addTool(getArtistTool); server.addTool(getArtistReleasesTool); server.addTool(getLabelTool); server.addTool(getLabelReleasesTool); server.addTool(searchTool); } var InventoryIdParamSchema = z.object({ id: z.number() }); var InventoryExportItemSchema = z.object({ status: z.string(), created_ts: z.string().nullable(), url: urlOrEmptySchema(), finished_ts: z.string().nullable(), download_url: urlOrEmptySchema(), filename: z.string(), id: z.number() }); var InventoryExportsResponseSchema = PaginatedResponseSchema( InventoryExportItemSchema, "items" ); // src/services/inventory.ts var InventoryService = class extends DiscogsService { constructor() { super("/inventory"); } /** * Download an inventory export as a CSV * * @param {InventoryIdParam} params - The parameters for the request * @returns {string} The inventory export as a CSV * @throws {DiscogsAuthenticationError} If the request is not authenticated * @throws {DiscogsResourceNotFoundError} If the inventory export does not exist * @throws {Error} If there's an unexpected error */ async downloadExport({ id }) { try { const response = await this.request(`/export/${id}/download`); return response; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to download inventory export: ${String(error)}`); } } /** * Request an export of your inventory as a CSV * * @returns {void} * @throws {DiscogsAuthenticationError} If the request is not authenticated * @throws {Error} If there's an unexpected error */ async export() { try { await this.request("/export", { method: "POST" }); } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to export inventory: ${String(error)}`); } } /** * Get details about an inventory export * * @param {InventoryIdParam} params - The parameters for the request * @returns {InventoryExportItem} The inventory export item * @throws {DiscogsAuthenticationError} If the request is not authenticated * @throws {DiscogsResourceNotFoundError} If the inventory export does not exist * @throws {Error} If there's an unexpected error */ async getExport({ id }) { try { const response = await this.request(`/export/${id}`); const validatedResponse = InventoryExportItemSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get inventory export: ${String(error)}`); } } /** * Get a list of all recent exports of your inventory * * @returns {InventoryExportsResponse} The inventory exports * @throws {DiscogsAuthenticationError} If the request is not authenticated * @throws {Error} If there's an unexpected error */ async getExports() { try { const response = await this.request("/export"); const validatedResponse = InventoryExportsResponseSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get inventory exports: ${String(error)}`); } } }; // src/tools/inventoryExport.ts var downloadInventoryExportTool = { name: "download_inventory_export", description: "Download an inventory export as a CSV", parameters: InventoryIdParamSchema, execute: async (args) => { try { const inventoryService = new InventoryService(); const csv = await inventoryService.downloadExport(args); return csv; } catch (error) { throw formatDiscogsError(error); } } }; var getInventoryExportTool = { name: "get_inventory_export", description: "Get details about an inventory export", parameters: InventoryIdParamSchema, execute: async (args) => { try { const inventoryService = new InventoryService(); const exportItem = await inventoryService.getExport(args); return JSON.stringify(exportItem); } catch (error) { throw formatDiscogsError(error); } } }; var getInventoryExportsTool = { name: "get_inventory_exports", description: "Get a list of all recent exports of your inventory", parameters: z.object({}), execute: async () => { try { const inventoryService = new InventoryService(); const exports = await inventoryService.getExports(); return JSON.stringify(exports); } catch (error) { throw formatDiscogsError(error); } } }; var inventoryExportTool = { name: "inventory_export", description: "Request an export of your inventory as a CSV", parameters: z.object({}), execute: async () => { try { const inventoryService = new InventoryService(); await inventoryService.export(); return "Inventory export requested"; } catch (error) { throw formatDiscogsError(error); } } }; function registerInventoryExportTool(server) { server.addTool(inventoryExportTool); server.addTool(getInventoryExportsTool); server.addTool(getInventoryExportTool); server.addTool(downloadInventoryExportTool); } var ConditionSchema = z.enum([ "Mint (M)", "Near Mint (NM or M-)", "Very Good Plus (VG+)", "Very Good (VG)", "Good Plus (G+)", "Good (G)", "Fair (F)", "Poor (P)" ]); var SleeveConditionSchema = z.enum([ ...ConditionSchema.options, "Generic", "Not Graded", "No Cover" ]); var OrderStatusSchema = z.enum([ "New Order", "Buyer Contacted", "Invoice Sent", "Payment Pending", "Payment Received", "Shipped", "Refund Sent", "Cancelled (Non-Paying Buyer)", "Cancelled (Item Unavailable)", `Cancelled (Per Buyer's Request)` ]); var ListingReleaseSchema = z.object({ catalog_number: z.string().optional(), resource_url: urlOrEmptySchema(), year: z.number(), id: z.number().int(), description: z.string(), images: z.array(ImageSchema).optional(), artist: z.string(), title: z.string(), format: z.string(), thumbnail: urlOrEmptySchema(), stats: z.object({ community: z.object({ in_wantlist: z.number().int(), in_collection: z.number().int() }), user: z.object({ in_wantlist: z.number().int(), in_collection: z.number().int() }).optional() }) }); var PriceSchema = z.object({ currency: CurrencyCodeSchema.optional(), value: z.number().optional() }); var OriginalPriceSchema = z.object({ curr_abbr: CurrencyCodeSchema.optional(), curr_id: z.number().optional(), formatted: z.string().optional(), value: z.number().optional() }); var OrderMessageSchema = z.object({ timestamp: z.string().optional(), message: z.string(), type: z.string().optional(), order: z.object({ id: z.number(), resource_url: urlOrEmptySchema() }), subject: z.string().optional(), refund: z.object({ amount: z.number(), order: z.object({ id: z.number(), resource_url: urlOrEmptySchema() }) }).optional(), from: z.object({ id: z.number().optional(), resource_url: urlOrEmptySchema(), username: z.string(), avatar_url: urlOrEmptySchema().optional() }).optional(), status_id: z.number().optional(), actor: z.object({ username: z.string(), resource_url: urlOrEmptySchema() }).optional(), original: z.number().optional(), new: z.number().optional() }); var SaleStatusSchema = z.enum(["For Sale", "Expired", "Draft", "Pending"]); var ListingSchema = z.object({ id: z.number(), resource_url: z.string().url(), uri: z.string().url(), status: SaleStatusSchema, condition: z.string(), sleeve_condition: z.string(), comments: z.string().optional(), ships_from: z.string(), posted: z.string(), allow_offers: z.boolean(), offer_submitted: z.boolean().optional(), audio: z.boolean(), price: PriceSchema, original_price: OriginalPriceSchema, shipping_price: PriceSchema.optional(), original_shipping_price: OriginalPriceSchema.optional(), seller: z.object({ id: z.number(), username: z.string(), resource_url: urlOrEmptySchema().optional(), avatar_url: urlOrEmptySchema().optional(), stats: z.object({ rating: z.string(), stars: z.number(), total: z.number() }), min_order_total: z.number(), html_url: urlOrEmptySchema(), uid: z.number(), url: urlOrEmptySchema(), payment: z.string(), shipping: z.string() }), release: ListingReleaseSchema }); var ListingIdParamSchema = z.object({ listing_id: z.number().int() }); var ListingGetParamsSchema = ListingIdParamSchema.extend({ curr_abbr: CurrencyCodeSchema.optional() }); var ListingNewParamsSchema = z.object({ release_id: z.number().int(), condition: ConditionSchema, sleeve_condition: SleeveConditionSchema.optional(), price: z.number(), comments: z.string().optional(), allow_offers: z.boolean().optional(), status: SaleStatusSchema, external_id: z.string().optional(), location: z.string().optional(), weight: z.number().optional(), format_quantity: z.number().optional() }); var ListingNewResponseSchema = z.object({ listing_id: z.number().int(), resource_url: z.string().url() }); var ListingUpdateParamsSchema = ListingIdParamSchema.merge(ListingNewParamsSchema); var OrderIdParamSchema = z.object({ order_id: z.number() }); var OrderCreateMessageParamsSchema = OrderIdParamSchema.extend({ message: z.string().optional(), status: OrderStatusSchema.optional() }); var OrderEditParamsSchema = OrderIdParamSchema.extend({ status: OrderStatusSchema.optional(), shipping: z.number().optional() }); var OrderMessagesParamsSchema = QueryParamsSchema().merge(OrderIdParamSchema); var OrderMessagesResponseSchema = PaginatedResponseSchema(OrderMessageSchema, "messages"); var OrdersParamsSchema = z.object({ status: OrderStatusSchema.optional(), created_after: z.string().optional(), created_before: z.string().optional(), archived: z.boolean().optional() }).merge(QueryParamsSchema(["id", "buyer", "created", "status", "last_activity"])); var OrderResponseSchema = z.object({ id: z.number(), resource_url: urlOrEmptySchema(), messages_url: urlOrEmptySchema(), uri: urlOrEmptySchema(), status: OrderStatusSchema, next_status: z.array(OrderStatusSchema), fee: PriceSchema, created: z.string(), items: z.array( z.object({ release: z.object({ id: z.number(), description: z.string().optional() }), price: PriceSchema, media_condition: ConditionSchema, sleeve_condition: SleeveConditionSchema.optional(), id: z.number() }) ), shipping: z.object({ currency: CurrencyCodeSchema, method: z.string(), value: z.number() }), shipping_address: z.string(), address_instructions: z.string().optional(), archived: z.boolean().optional(), seller: z.object({ id: z.number(), username: z.string(), resource_url: urlOrEmptySchema().optional() }), last_activity: z.string().optional(), buyer: z.object({ id: z.number(), username: z.string(), resource_url: urlOrEmptySchema().optional() }), total: PriceSchema }); var OrdersResponseSchema = PaginatedResponseSchema(OrderResponseSchema, "orders"); var ReleaseStatsResponseSchema = z.object({ lowest_price: PriceSchema.nullable().optional(), num_for_sale: z.number().nullable().optional(), blocked_from_sale: z.boolean() }); // src/services/marketplace.ts var MarketplaceService = class extends DiscogsService { constructor() { super("/marketplace"); } /** * Create a new marketplace listing * * @param params - Parameters containing the listing data * @returns {ListingNewResponse} The listing information * @throws {DiscogsAuthenticationError} If the user is not authenticated * @throws {DiscogsPermissionError} If the user does not have permission to create a listing * @throws {Error} If there's an unexpected error */ async createListing(params) { try { const response = await this.request(`/listings`, { method: "POST", body: params }); const validatedResponse = ListingNewResponseSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to create listing: ${String(error)}`); } } /** * Adds a new message to the order's message log * * @param params - Parameters containing the order ID and the message data * @returns {OrderMessageResponse} The order message information * @throws {DiscogsAuthenticationError} If the user is not authenticated * @throws {DiscogsPermissionError} If the user does not have permission to create a message * @throws {DiscogsResourceNotFoundError} If the order cannot be found * @throws {Error} If there's an unexpected error */ async createOrderMessage({ order_id, ...body }) { try { const response = await this.request(`/orders/${order_id}/messages`, { method: "POST", body }); const validatedResponse = OrderMessageSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to create order message: ${String(error)}`); } } /** * Delete a listing from the marketplace * * @param params - Parameters containing the listing ID * @throws {DiscogsAuthenticationError} If the user is not authenticated * @throws {DiscogsPermissionError} If the user does not have permission to delete a listing * @throws {DiscogsResourceNotFoundError} If the listing cannot be found * @throws {Error} If there's an unexpected error */ async deleteListing({ listing_id }) { try { await this.request(`/listings/${listing_id}`, { method: "DELETE" }); } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to delete listing: ${String(error)}`); } } /** * The Listing resource allows you to view Marketplace listings * * @param params - Parameters containing the listing ID and optional currency code * @returns {Listing} The listing information * @throws {DiscogsResourceNotFoundError} If the listing cannot be found * @throws {Error} If there's an unexpected error */ async getListing({ listing_id, ...options }) { try { const response = await this.request(`/listings/${listing_id}`, { params: options }); const validatedResponse = ListingSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } throw new Error(`Failed to get listing: ${String(error)}`); } } /** * Get a marketplace order * * @param params - Parameters containing the order ID * @throws {DiscogsAuthenticationError} If the user is not authenticated * @throws {DiscogsPermissionError} If the user does not have permission to view the order * @throws {DiscogsResourceNotFoundError} If the order cannot be found * @throws {Error} If there's an unexpected error * @returns {OrderResponse} The order information */ async getOrder({ order_id }) { try { const response = await this.request(`/orders/${order_id}`); const validatedResponse = OrderResponseSchema.parse(response); return validatedResponse; } catch (error) { if (isDiscogsError(error)) { throw error; } t