Skip to main content
Glama
charlielockyer-rice

tscircuit MCP Server

tscircuit-api.ts13.1 kB
// Node.js v18+ has built-in fetch, no import needed import { RegistrySearchResponse, PackageDetails, SearchComponentsParams, GetPackageDetailsParams, GetPackageCodeParams, AnalyzeComponentParams, ErrorResponse, CacheEntry, ComponentCategory } from './types.js'; const REGISTRY_BASE_URL = 'https://registry-api.tscircuit.com'; const CACHE_TTL = 5 * 60 * 1000; // 5 minutes const REQUEST_TIMEOUT = 10000; // 10 seconds class TscircuitAPI { private cache = new Map<string, CacheEntry<any>>(); private async fetchWithTimeout(url: string, options: any = {}, timeout: number = REQUEST_TIMEOUT): Promise<any> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'tscircuit-mcp/1.0.0', 'Accept': 'application/json', 'Content-Type': 'application/json', ...options.headers }, ...options }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); throw error; } } private getCacheKey(endpoint: string, params?: Record<string, any>): string { const paramString = params ? JSON.stringify(params) : ''; return `${endpoint}:${paramString}`; } private getFromCache<T>(key: string): T | null { const entry = this.cache.get(key); if (!entry) return null; const now = Date.now(); if (now - entry.timestamp > entry.ttl) { this.cache.delete(key); return null; } return entry.data as T; } private setCache<T>(key: string, data: T, ttl: number = CACHE_TTL): void { this.cache.set(key, { data, timestamp: Date.now(), ttl }); } async searchPackages(params: SearchComponentsParams): Promise<RegistrySearchResponse> { const cacheKey = this.getCacheKey('search', params); const cached = this.getFromCache<RegistrySearchResponse>(cacheKey); if (cached) return cached; const url = `${REGISTRY_BASE_URL}/packages/search`; try { const response = await this.fetchWithTimeout(url, { method: 'POST', body: JSON.stringify({ query: params.query }) }, REQUEST_TIMEOUT); if (!response.ok) { throw new Error(`Search failed: ${response.status} ${response.statusText}`); } const responseData = await response.json(); if (!responseData.ok) { throw new Error(`Search failed: ${responseData.error?.message || 'Unknown error'}`); } // Transform tscircuit API response to match expected format const tscircuitPackages = responseData.packages || []; // Filter by category if specified let filteredPackages = tscircuitPackages; if (params.category) { filteredPackages = tscircuitPackages.filter((pkg: any) => pkg.name?.toLowerCase().includes(params.category!.toLowerCase()) || pkg.description?.toLowerCase().includes(params.category!.toLowerCase()) || pkg.ai_description?.toLowerCase().includes(params.category!.toLowerCase()) ); } // Apply limit if (params.limit) { filteredPackages = filteredPackages.slice(0, params.limit); } // Transform to expected format const transformedData: RegistrySearchResponse = { objects: filteredPackages.map((pkg: any) => ({ package: { name: pkg.name, version: pkg.latest_version, description: pkg.description || pkg.ai_description || '', keywords: [], // tscircuit API doesn't return keywords in search links: { npm: `https://www.npmjs.com/package/@tsci/${pkg.name}`, homepage: pkg.website || undefined, repository: pkg.website || undefined } }, score: { final: pkg.star_count || 0, detail: { quality: 1, popularity: pkg.star_count || 0, maintenance: 1 } } })), total: filteredPackages.length, time: new Date().toISOString() }; this.setCache(cacheKey, transformedData); return transformedData; } catch (error) { throw new Error(`Failed to search packages: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getPackageDetails(params: GetPackageDetailsParams): Promise<PackageDetails> { const cacheKey = this.getCacheKey('package', params); const cached = this.getFromCache<PackageDetails>(cacheKey); if (cached) return cached; // For now, search for the package to get its details since there's no direct package endpoint const searchResult = await this.searchPackages({ query: params.packageName, limit: 50 }); const exactMatch = searchResult.objects.find(obj => obj.package.name === params.packageName || obj.package.name.endsWith(`/${params.packageName}`) || obj.package.name.includes(params.packageName) ); if (!exactMatch) { throw new Error(`Package "${params.packageName}" not found`); } // Transform to PackageDetails format const packageDetails: PackageDetails = { name: exactMatch.package.name, version: exactMatch.package.version, description: exactMatch.package.description, keywords: exactMatch.package.keywords, links: exactMatch.package.links, // Add some additional fields that might be useful versions: [exactMatch.package.version], license: 'unset', // tscircuit packages typically don't set license downloads: { weekly: 0, monthly: 0 } }; this.setCache(cacheKey, packageDetails); return packageDetails; } async getPackageCode(params: GetPackageCodeParams): Promise<string> { const cacheKey = this.getCacheKey('code', params); const cached = this.getFromCache<string>(cacheKey); if (cached) return cached; try { // First, list the files in the package const listUrl = `${REGISTRY_BASE_URL}/package_files/list`; const listResponse = await this.fetchWithTimeout(listUrl, { method: 'POST', body: JSON.stringify({ package_name: params.packageName, use_latest_version: true }) }, REQUEST_TIMEOUT); if (!listResponse.ok) { throw new Error(`Failed to list package files: ${listResponse.status} ${listResponse.statusText}`); } const listData = await listResponse.json(); if (!listData.ok) { throw new Error(`Failed to list package files: ${listData.error?.message || 'Unknown error'}`); } // Find the index.tsx file (main component file) const indexFile = listData.package_files?.find((file: any) => file.file_path === 'index.tsx' || file.file_path === 'index.ts' || file.file_path === 'src/index.tsx' || file.file_path === 'src/index.ts' ); if (!indexFile) { throw new Error(`No index file found in package "${params.packageName}"`); } // Download the index file const downloadUrl = `${REGISTRY_BASE_URL}/package_files/download?package_file_id=${indexFile.package_file_id}`; const downloadResponse = await this.fetchWithTimeout(downloadUrl, { method: 'GET' }, REQUEST_TIMEOUT); if (!downloadResponse.ok) { throw new Error(`Failed to download package file: ${downloadResponse.status} ${downloadResponse.statusText}`); } const code = await downloadResponse.text(); if (!code) { throw new Error(`No code found in package "${params.packageName}"`); } this.setCache(cacheKey, code); return code; } catch (error) { throw new Error(`Failed to fetch package code: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async analyzeComponent(params: AnalyzeComponentParams): Promise<any> { const packageDetails = await this.getPackageDetails({ packageName: params.packageName }); // Extract component analysis from package metadata const analysis = { name: packageDetails.name, description: packageDetails.description, category: this.inferCategory(packageDetails), specifications: packageDetails.tscircuit?.specifications || {}, footprint: packageDetails.tscircuit?.footprint, keywords: packageDetails.keywords || [], repository: packageDetails.links?.repository, npm: packageDetails.links?.npm, version: packageDetails.version, license: packageDetails.license, dependencies: packageDetails.dependencies || {}, estimated_specs: this.extractSpecsFromDescription(packageDetails.description, packageDetails.keywords || []) }; return analysis; } private inferCategory(pkg: PackageDetails): ComponentCategory { const keywords = pkg.keywords || []; const description = pkg.description?.toLowerCase() || ''; const name = pkg.name.toLowerCase(); // Check explicit category from tscircuit metadata if (pkg.tscircuit?.category) { return pkg.tscircuit.category as ComponentCategory; } // Infer from keywords and description if (keywords.includes('resistor') || description.includes('resistor') || name.includes('resistor')) return 'resistor'; if (keywords.includes('capacitor') || description.includes('capacitor') || name.includes('capacitor')) return 'capacitor'; if (keywords.includes('inductor') || description.includes('inductor') || name.includes('inductor')) return 'inductor'; if (keywords.includes('diode') || description.includes('diode') || name.includes('diode')) return 'diode'; if (keywords.includes('transistor') || description.includes('transistor') || name.includes('transistor')) return 'transistor'; if (keywords.includes('ic') || description.includes('integrated circuit') || name.includes('ic')) return 'ic'; if (keywords.includes('connector') || description.includes('connector') || name.includes('connector')) return 'connector'; if (keywords.includes('led') || description.includes('led') || name.includes('led')) return 'led'; if (keywords.includes('switch') || description.includes('switch') || name.includes('switch')) return 'switch'; if (keywords.includes('sensor') || description.includes('sensor') || name.includes('sensor')) return 'sensor'; if (keywords.includes('crystal') || description.includes('crystal') || name.includes('crystal')) return 'crystal'; if (keywords.includes('relay') || description.includes('relay') || name.includes('relay')) return 'relay'; if (keywords.includes('transformer') || description.includes('transformer') || name.includes('transformer')) return 'transformer'; if (keywords.includes('fuse') || description.includes('fuse') || name.includes('fuse')) return 'fuse'; if (keywords.includes('battery') || description.includes('battery') || name.includes('battery')) return 'battery'; if (keywords.includes('motor') || description.includes('motor') || name.includes('motor')) return 'motor'; if (keywords.includes('display') || description.includes('display') || name.includes('display')) return 'display'; if (keywords.includes('antenna') || description.includes('antenna') || name.includes('antenna')) return 'antenna'; return 'other'; } private extractSpecsFromDescription(description: string, keywords: string[]): Record<string, any> { const specs: Record<string, any> = {}; const text = `${description} ${keywords.join(' ')}`.toLowerCase(); // Extract voltage specifications const voltageMatch = text.match(/(\d+(?:\.\d+)?)\s*v(?:olt)?/); if (voltageMatch) { specs.voltage = parseFloat(voltageMatch[1]); } // Extract current specifications const currentMatch = text.match(/(\d+(?:\.\d+)?)\s*(?:ma|amp|ampere)/); if (currentMatch) { specs.current = parseFloat(currentMatch[1]); } // Extract resistance specifications const resistanceMatch = text.match(/(\d+(?:\.\d+)?)\s*(?:ohm|Ω|k|m)/); if (resistanceMatch) { specs.resistance = parseFloat(resistanceMatch[1]); } // Extract capacitance specifications const capacitanceMatch = text.match(/(\d+(?:\.\d+)?)\s*(?:pf|nf|uf|farad)/); if (capacitanceMatch) { specs.capacitance = parseFloat(capacitanceMatch[1]); } // Extract package/footprint information const packageMatch = text.match(/(sot-23|sot-89|to-220|dip|smd|0805|0603|1206|bga|qfn|soic|ssop|tssop|lqfp)/); if (packageMatch) { specs.footprint = packageMatch[1].toUpperCase(); } return specs; } clearCache(): void { this.cache.clear(); } getCacheStats(): { size: number; keys: string[] } { return { size: this.cache.size, keys: Array.from(this.cache.keys()) }; } } export default TscircuitAPI;

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/charlielockyer-rice/tscircuit-mcp'

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