Thingiverse MCP Server
by gpaul-mcp
Verified
- MCP_thingiverse
- src
- class
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());
}
}