Thingiverse MCP Server

by gpaul-mcp
Verified
import { Category } from '@/types/category'; import { ThingFile } from '@/types/files'; import { Thing, ThingiverseSearchResponse } from '@/types/things'; import { Browser } from 'puppeteer'; import puppeteer from 'puppeteer-extra'; import StealthPlugin from 'puppeteer-extra-plugin-stealth'; puppeteer.use(StealthPlugin()); export default class ThingiversePuppeteer { private readonly baseUrl = 'https://api.thingiverse.com'; private browser: Browser | null = null; private token: string; constructor(appToken: string) { if (!appToken) { throw new Error('App token is required for Thingiverse API'); } this.token = appToken; } async init() { if (!this.browser) { this.browser = await puppeteer.launch({ headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--disable-gpu', ], }); } } async close() { if (this.browser) { await this.browser.close(); this.browser = null; } } private async makeRequest<T>(url: string): Promise<T> { if (!this.browser) { throw new Error('Browser not initialized. Call init() first.'); } const page = await this.browser.newPage(); try { await page.setExtraHTTPHeaders({ Authorization: `Bearer ${this.token}`, Accept: 'application/json', 'Accept-Language': 'en-US,en;q=0.9', 'Cache-Control': 'no-cache', Pragma: 'no-cache', }); await page.setViewport({ width: 1280, height: 800 }); await page.setUserAgent( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', ); const response = await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000, }); if (!response) { throw new Error('No response received'); } const status = response.status(); if (status !== 200) { throw new Error(`API returned status code ${status}`); } const jsonContent = await response.text(); return JSON.parse(jsonContent) as T; } catch (error) { console.error(`Puppeteer request failed for URL: ${url}`); console.error(error); throw error; } finally { await page.close(); } } // API Methods public async searchThings(term: string, categoryId?: string): Promise<ThingiverseSearchResponse> { const url = categoryId ? `${this.baseUrl}/search/${term}/?type=things&category_id=${categoryId}&sort=relevant&per_page=100` : `${this.baseUrl}/search/${term}/?type=things&sort=relevant&per_page=100`; return this.makeRequest<ThingiverseSearchResponse>(url); } public async getFilesFromThing(thingId: number): Promise<ThingFile[]> { return this.makeRequest<ThingFile[]>(`${this.baseUrl}/things/${thingId}/files`); } public async getRandomThings(): Promise<Thing[]> { return this.makeRequest<Thing[]>(`${this.baseUrl}/things/0/random`); } public async getCategories(): Promise<Category[]> { return this.makeRequest<Category[]>(`${this.baseUrl}/categories?page=1&per_page=30`); } public async getRandomThingsFromCategory(slug: string): Promise<Thing[]> { return this.makeRequest<Thing[]>( `${this.baseUrl}/categories/${slug}/things?page=1&per_page=100&sort=popular`, ); } public async getThingById(thingId: number): Promise<Thing> { return this.makeRequest<Thing>(`${this.baseUrl}/things/${thingId}`); } public async searchThingsPaginated( term: string, page = 1, perPage = 20, sortBy: 'relevant' | 'popular' | 'newest' | 'makes' | 'likes' = 'relevant', categoryId?: string, ): Promise<ThingiverseSearchResponse> { const url = new URL(`${this.baseUrl}/search/${term}/`); // Add query parameters url.searchParams.append('type', 'things'); url.searchParams.append('sort', sortBy); url.searchParams.append('page', page.toString()); url.searchParams.append('per_page', perPage.toString()); if (categoryId) { url.searchParams.append('category_id', categoryId); } return this.makeRequest<ThingiverseSearchResponse>(url.toString()); } public async getFeaturedThings( type: 'featured' | 'popular' | 'newest' | 'staff-picks' = 'featured', page = 1, perPage = 100, ): Promise<Thing[]> { let endpoint: string; switch (type) { case 'featured': endpoint = `${this.baseUrl}/featured`; break; case 'popular': endpoint = `${this.baseUrl}/popular`; break; case 'newest': endpoint = `${this.baseUrl}/newest`; break; case 'staff-picks': endpoint = `${this.baseUrl}/collections/featured`; break; default: endpoint = `${this.baseUrl}/featured`; } const url = new URL(endpoint); url.searchParams.append('page', page.toString()); url.searchParams.append('per_page', perPage.toString()); return this.makeRequest<Thing[]>(url.toString()); } }