Skip to main content
Glama

Pub.dev MCP Server

by devqxi
pubdev-mcp.ts17.9 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; import { fileURLToPath } from 'url'; import { dirname } from 'path'; interface PackageInfo { name: string; version: string; description?: string; homepage?: string; repository?: string; publishedAt: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string>; } interface PackageVersion { version: string; publishedAt: string; description?: string; changelog?: string; } interface DocumentationChange { type: 'readme' | 'changelog' | 'example' | 'api_docs'; content: string; lastModified?: string; } class PubDevMCPServer { private server: Server; private packageCache: Map<string, { data: any; timestamp: number }> = new Map(); // Changed to 'any' to be more flexible private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes constructor() { this.server = new Server( { name: "pubdev-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_package_info", description: "Get detailed information about a Dart/Flutter package from pub.dev", inputSchema: { type: "object", properties: { packageName: { type: "string", description: "Name of the package to retrieve information for" } }, required: ["packageName"] } }, { name: "check_package_updates", description: "Check for updates to a specific package or compare versions", inputSchema: { type: "object", properties: { packageName: { type: "string", description: "Name of the package to check for updates" }, currentVersion: { type: "string", description: "Current version to compare against (optional)" } }, required: ["packageName"] } }, { name: "get_package_versions", description: "Get all available versions of a package with their release dates", inputSchema: { type: "object", properties: { packageName: { type: "string", description: "Name of the package to get versions for" }, limit: { type: "number", description: "Maximum number of versions to return (default: 10)" } }, required: ["packageName"] } }, { name: "get_documentation_changes", description: "Get documentation content and detect changes for a package", inputSchema: { type: "object", properties: { packageName: { type: "string", description: "Name of the package to get documentation for" }, version: { type: "string", description: "Specific version (optional, defaults to latest)" }, docType: { type: "string", enum: ["readme", "changelog", "example", "api_docs"], description: "Type of documentation to retrieve" } }, required: ["packageName"] } }, { name: "compare_package_versions", description: "Compare two versions of a package and show differences", inputSchema: { type: "object", properties: { packageName: { type: "string", description: "Name of the package to compare" }, fromVersion: { type: "string", description: "Source version to compare from" }, toVersion: { type: "string", description: "Target version to compare to" } }, required: ["packageName", "fromVersion", "toVersion"] } }, { name: "search_packages", description: "Search for packages on pub.dev with filters", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, sort: { type: "string", enum: ["top", "text", "created", "updated", "popularity", "points", "likes"], description: "Sort order for results" }, page: { type: "number", description: "Page number for pagination (default: 1)" } }, required: ["query"] } } ] as Tool[] }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Add null check for args if (!args) { throw new Error('Arguments are required'); } try { switch (name) { case "get_package_info": return await this.getPackageInfo(args.packageName as string); case "check_package_updates": return await this.checkPackageUpdates( args.packageName as string, args.currentVersion as string ); case "get_package_versions": return await this.getPackageVersions( args.packageName as string, args.limit as number ); case "get_documentation_changes": return await this.getDocumentationChanges( args.packageName as string, args.version as string, args.docType as string ); case "compare_package_versions": return await this.comparePackageVersions( args.packageName as string, args.fromVersion as string, args.toVersion as string ); case "search_packages": return await this.searchPackages( args.query as string, args.sort as string, args.page as number ); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` } ] }; } }); } private async fetchWithCache<T>(url: string, cacheKey: string): Promise<T> { const cached = this.packageCache.get(cacheKey); const now = Date.now(); if (cached && (now - cached.timestamp) < this.CACHE_DURATION) { return cached.data as T; } const response = await fetch(url, { headers: { 'User-Agent': 'MCP-PubDev-Server/1.0.0', 'Accept': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json() as T; this.packageCache.set(cacheKey, { data, timestamp: now }); return data; } private async getPackageInfo(packageName: string) { const url = `https://pub.dev/api/packages/${packageName}`; const data = await this.fetchWithCache<any>(url, `package-${packageName}`); const packageInfo: PackageInfo = { name: data.name, version: data.latest.version, description: data.latest.pubspec?.description, homepage: data.latest.pubspec?.homepage, repository: data.latest.pubspec?.repository, publishedAt: data.latest.published, dependencies: data.latest.pubspec?.dependencies, devDependencies: data.latest.pubspec?.dev_dependencies }; return { content: [ { type: "text", text: JSON.stringify({ package: packageInfo, stats: { likes: data.likes, points: data.points, popularity: data.popularity }, publishers: data.publishers, uploaders: data.uploaders }, null, 2) } ] }; } private async checkPackageUpdates(packageName: string, currentVersion?: string) { const url = `https://pub.dev/api/packages/${packageName}`; const data = await this.fetchWithCache<any>(url, `package-${packageName}`); const latestVersion = data.latest.version; const latestPublished = data.latest.published; let updateStatus = { packageName, currentVersion: currentVersion || 'unknown', latestVersion, latestPublished, updateAvailable: false, versionsBehind: 0 }; if (currentVersion) { updateStatus.updateAvailable = this.compareVersions(currentVersion, latestVersion) < 0; // Get version history to count versions behind const versionsUrl = `https://pub.dev/api/packages/${packageName}/versions`; const versionsData = await this.fetchWithCache<any>(versionsUrl, `versions-${packageName}`); const currentIndex = versionsData.versions.findIndex((v: any) => v.version === currentVersion); const latestIndex = versionsData.versions.findIndex((v: any) => v.version === latestVersion); if (currentIndex > -1 && latestIndex > -1) { updateStatus.versionsBehind = currentIndex - latestIndex; } } return { content: [ { type: "text", text: JSON.stringify(updateStatus, null, 2) } ] }; } private async getPackageVersions(packageName: string, limit: number = 10) { const url = `https://pub.dev/api/packages/${packageName}/versions`; const data = await this.fetchWithCache<any>(url, `versions-${packageName}`); const versions: PackageVersion[] = data.versions .slice(0, limit) .map((v: any) => ({ version: v.version, publishedAt: v.published, description: v.pubspec?.description })); return { content: [ { type: "text", text: JSON.stringify({ packageName, totalVersions: data.versions.length, versions }, null, 2) } ] }; } private async getDocumentationChanges(packageName: string, version?: string, docType: string = 'readme') { let baseUrl: string; if (version) { baseUrl = `https://pub.dev/packages/${packageName}/versions/${version}`; } else { baseUrl = `https://pub.dev/packages/${packageName}`; } let docUrl: string; let contentType: string; switch (docType) { case 'readme': docUrl = `${baseUrl}/readme`; contentType = 'README'; break; case 'changelog': docUrl = `${baseUrl}/changelog`; contentType = 'CHANGELOG'; break; case 'example': docUrl = `${baseUrl}/example`; contentType = 'Example'; break; case 'api_docs': docUrl = `https://pub.dev/documentation/${packageName}/${version || 'latest'}/`; contentType = 'API Documentation'; break; default: throw new Error(`Unsupported documentation type: ${docType}`); } try { const response = await fetch(docUrl); let content: string; if (response.ok) { content = await response.text(); // Extract meaningful content from HTML if needed if (docType !== 'api_docs') { content = this.extractTextFromHtml(content); } } else { content = `${contentType} not available for this package/version`; } const docChange: DocumentationChange = { type: docType as any, content: content.substring(0, 5000), // Limit content size lastModified: response.headers.get('last-modified') || undefined }; return { content: [ { type: "text", text: JSON.stringify({ packageName, version: version || 'latest', documentationType: docType, documentation: docChange }, null, 2) } ] }; } catch (error) { throw new Error(`Failed to fetch documentation: ${error}`); } } private async comparePackageVersions(packageName: string, fromVersion: string, toVersion: string) { const versionsUrl = `https://pub.dev/api/packages/${packageName}/versions`; const data = await this.fetchWithCache<any>(versionsUrl, `versions-${packageName}`); const fromVersionData = data.versions.find((v: any) => v.version === fromVersion); const toVersionData = data.versions.find((v: any) => v.version === toVersion); if (!fromVersionData || !toVersionData) { throw new Error('One or both versions not found'); } const comparison = { packageName, comparison: { from: { version: fromVersion, published: fromVersionData.published, dependencies: fromVersionData.pubspec?.dependencies || {}, devDependencies: fromVersionData.pubspec?.dev_dependencies || {} }, to: { version: toVersion, published: toVersionData.published, dependencies: toVersionData.pubspec?.dependencies || {}, devDependencies: toVersionData.pubspec?.dev_dependencies || {} } }, changes: { dependencyChanges: this.compareDependencies( fromVersionData.pubspec?.dependencies || {}, toVersionData.pubspec?.dependencies || {} ), devDependencyChanges: this.compareDependencies( fromVersionData.pubspec?.dev_dependencies || {}, toVersionData.pubspec?.dev_dependencies || {} ) } }; return { content: [ { type: "text", text: JSON.stringify(comparison, null, 2) } ] }; } private async searchPackages(query: string, sort: string = 'top', page: number = 1) { const params = new URLSearchParams({ q: query, sort: sort, page: page.toString() }); const url = `https://pub.dev/api/search?${params}`; const data = await this.fetchWithCache<any>(url, `search-${query}-${sort}-${page}`); const results = { query, sort, page, totalResults: data.count, packages: data.packages.map((pkg: any) => ({ name: pkg.package, version: pkg.latest.version, description: pkg.latest.pubspec?.description, points: pkg.points, likes: pkg.likes, popularity: pkg.popularity, publishedAt: pkg.latest.published })) }; return { content: [ { type: "text", text: JSON.stringify(results, null, 2) } ] }; } private compareVersions(version1: string, version2: string): number { const v1Parts = version1.split('.').map(Number); const v2Parts = version2.split('.').map(Number); for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { const v1Part = v1Parts[i] || 0; const v2Part = v2Parts[i] || 0; if (v1Part < v2Part) return -1; if (v1Part > v2Part) return 1; } return 0; } private compareDependencies(oldDeps: Record<string, string>, newDeps: Record<string, string>) { const changes = { added: [] as string[], removed: [] as string[], updated: [] as { package: string; from: string; to: string }[] }; // Find added dependencies for (const [pkg, version] of Object.entries(newDeps)) { if (!(pkg in oldDeps)) { changes.added.push(`${pkg}: ${version}`); } } // Find removed and updated dependencies for (const [pkg, oldVersion] of Object.entries(oldDeps)) { if (!(pkg in newDeps)) { changes.removed.push(`${pkg}: ${oldVersion}`); } else if (newDeps[pkg] !== oldVersion) { changes.updated.push({ package: pkg, from: oldVersion, to: newDeps[pkg] }); } } return changes; } private extractTextFromHtml(html: string): string { // Simple HTML tag removal - in production, consider using a proper HTML parser return html .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') .replace(/<[^>]*>/g, '') .replace(/\s+/g, ' ') .trim(); } async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Pub.dev MCP Server running on stdio"); } } // Start the server const server = new PubDevMCPServer(); server.start().catch((error) => { console.error("Failed to start server:", error); process.exit(1); }); export default PubDevMCPServer;

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/devqxi/pubdev-mcp-server'

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