Skip to main content
Glama
photos.ts15.8 kB
// Photo search and retrieval tools import { z } from "zod"; import { UnsplashClient } from "../services/unsplash-client.js"; import { SearchPhotosSchema, GetPhotoSchema, GetRandomPhotoSchema, ListPhotosSchema, DownloadPhotoSchema, GetPhotoStatisticsSchema } from "../schemas/index.js"; import { SearchPhotosResponse, UnsplashPhoto, PhotoStatistics, ResponseFormat } from "../types.js"; import { formatPhotoMarkdown, formatPhotosListMarkdown, formatStatisticsMarkdown, truncateIfNeeded } from "../services/formatters.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; export function registerPhotoTools(server: McpServer, client: UnsplashClient): void { // Search photos server.registerTool( "unsplash_search_photos", { title: "Search Unsplash Photos", description: `Search for photos on Unsplash by keywords with advanced filtering options. This tool searches through millions of high-quality, free-to-use photos from the Unsplash platform. You can filter by orientation, color, content safety level, and sort by relevance or recency. Args: - query (string): Search keywords (e.g., "mountain sunset", "coffee cup", "architecture") - page (number): Page number for pagination (default: 1) - per_page (number): Results per page, 1-30 (default: 10) - orientation (string, optional): Filter by 'landscape', 'portrait', or 'squarish' - content_filter (string): Safety level 'low' (default) or 'high' - color (string, optional): Filter by color name (e.g., 'black', 'white', 'yellow', 'red') - order_by (string): Sort by 'relevant' (default) or 'latest' - response_format (string): Output as 'markdown' (default) or 'json' Returns: For JSON format: Complete search results with metadata { "total": number, // Total matching photos "total_pages": number, // Total pages available "page": number, // Current page "per_page": number, // Results per page "results": [ // Array of photo objects { "id": string, "width": number, "height": number, "color": string, "likes": number, "description": string, "alt_description": string, "urls": { "raw": string, "full": string, "regular": string, "small": string, "thumb": string }, "links": { "html": string, // Unsplash page URL "download": string }, "user": { "name": string, "username": string } } ] } Examples: - Search for nature photos: query="forest waterfall" - Find portrait photos: query="portrait", orientation="portrait" - Latest tech photos: query="technology", order_by="latest" Note: All photos from Unsplash are free to use. Attribution to the photographer is appreciated but not required.`, inputSchema: SearchPhotosSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params: z.infer<typeof SearchPhotosSchema>) => { try { const data = await client.makeRequest<SearchPhotosResponse>("/search/photos", { query: params.query, page: params.page, per_page: params.per_page, ...(params.orientation && { orientation: params.orientation }), ...(params.color && { color: params.color }), content_filter: params.content_filter, order_by: params.order_by }); if (data.results.length === 0) { return { content: [{ type: "text", text: `No photos found for query: "${params.query}"` }] }; } const output = { total: data.total, total_pages: data.total_pages, page: params.page, per_page: params.per_page, results: data.results }; if (params.response_format === ResponseFormat.JSON) { return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }], structuredContent: output }; } // Markdown format let markdown = `# Search Results for "${params.query}"\n\n`; markdown += `Found ${data.total.toLocaleString()} photos (Page ${params.page} of ${data.total_pages})\n\n`; markdown += formatPhotosListMarkdown(data.results); if (params.page < data.total_pages) { markdown += `\n\n*Use page=${params.page + 1} to see more results*`; } return { content: [{ type: "text", text: truncateIfNeeded(markdown) }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error searching photos: ${errorMessage}` }], isError: true }; } } ); // Get photo by ID server.registerTool( "unsplash_get_photo", { title: "Get Photo Details", description: `Retrieve detailed information about a specific Unsplash photo by its ID. This tool fetches complete metadata for a photo including dimensions, photographer info, EXIF data, location, tags, and download URLs. Args: - id (string): Unsplash photo ID (e.g., "LBI7cgq3pbM") - response_format (string): Output as 'markdown' (default) or 'json' Returns: For JSON format: Complete photo object with all metadata { "id": string, "width": number, "height": number, "color": string, "blur_hash": string, "likes": number, "description": string, "alt_description": string, "created_at": string, "urls": { ... }, "links": { ... }, "user": { ... }, "exif": { ... }, // Camera settings if available "location": { ... }, // Photo location if available "tags": [ ... ] } Examples: - Get photo details: id="LBI7cgq3pbM" Use unsplash_search_photos first to find photo IDs.`, inputSchema: GetPhotoSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params: z.infer<typeof GetPhotoSchema>) => { try { const photo = await client.makeRequest<UnsplashPhoto>(`/photos/${params.id}`); if (params.response_format === ResponseFormat.JSON) { return { content: [{ type: "text", text: JSON.stringify(photo, null, 2) }], structuredContent: photo }; } const markdown = formatPhotoMarkdown(photo); return { content: [{ type: "text", text: markdown }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error fetching photo: ${errorMessage}` }], isError: true }; } } ); // Get random photo(s) server.registerTool( "unsplash_get_random", { title: "Get Random Photos", description: `Get one or more random photos from Unsplash, optionally filtered by search query or orientation. This tool is perfect for getting inspiration, placeholder images, or diverse photo content. You can narrow the selection with search terms. Args: - query (string, optional): Search terms to filter random selection - orientation (string, optional): Filter by 'landscape', 'portrait', or 'squarish' - content_filter (string): Safety level 'low' (default) or 'high' - count (number): Number of random photos (1-30, default: 1) - response_format (string): Output as 'markdown' (default) or 'json' Returns: For JSON format: Array of photo objects For Markdown: Formatted list of photos with details Examples: - Single random photo: (no parameters) - Random nature photos: query="nature", count=5 - Random portraits: orientation="portrait", count=3 Note: Each request returns different random photos.`, inputSchema: GetRandomPhotoSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, // Not idempotent - returns different results openWorldHint: true } }, async (params: z.infer<typeof GetRandomPhotoSchema>) => { try { const requestParams: Record<string, string | number | boolean> = { content_filter: params.content_filter, count: params.count }; if (params.query) { requestParams.query = params.query; } if (params.orientation) { requestParams.orientation = params.orientation; } const data = await client.makeRequest<UnsplashPhoto | UnsplashPhoto[]>( "/photos/random", requestParams ); const photos = Array.isArray(data) ? data : [data]; if (params.response_format === ResponseFormat.JSON) { return { content: [{ type: "text", text: JSON.stringify(photos, null, 2) }], structuredContent: { photos } }; } let markdown = `# Random Photos${params.query ? ` (${params.query})` : ""}\n\n`; markdown += formatPhotosListMarkdown(photos); return { content: [{ type: "text", text: markdown }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error fetching random photos: ${errorMessage}` }], isError: true }; } } ); // List photos (editorial feed) server.registerTool( "unsplash_list_photos", { title: "List Editorial Photos", description: `List photos from Unsplash's editorial feed - curated high-quality photos. This tool provides access to Unsplash's main photo feed, sorted by latest, oldest, or most popular. Args: - page (number): Page number for pagination (default: 1) - per_page (number): Results per page, 1-30 (default: 10) - order_by (string): Sort by 'latest' (default), 'oldest', or 'popular' - response_format (string): Output as 'markdown' (default) or 'json' Returns: For JSON format: Array of photo objects For Markdown: Formatted list with pagination info Examples: - Latest photos: order_by="latest" - Most popular: order_by="popular", per_page=20`, inputSchema: ListPhotosSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params: z.infer<typeof ListPhotosSchema>) => { try { const photos = await client.makeRequest<UnsplashPhoto[]>("/photos", { page: params.page, per_page: params.per_page, order_by: params.order_by }); if (photos.length === 0) { return { content: [{ type: "text", text: "No photos found" }] }; } if (params.response_format === ResponseFormat.JSON) { return { content: [{ type: "text", text: JSON.stringify(photos, null, 2) }], structuredContent: { photos } }; } let markdown = `# Editorial Photos (${params.order_by})\n\n`; markdown += `Page ${params.page}\n\n`; markdown += formatPhotosListMarkdown(photos); return { content: [{ type: "text", text: truncateIfNeeded(markdown) }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error listing photos: ${errorMessage}` }], isError: true }; } } ); // Get photo statistics server.registerTool( "unsplash_get_statistics", { title: "Get Photo Statistics", description: `Get view, download, and like statistics for a specific photo. This tool provides detailed analytics including total counts and recent trends for a photo's popularity. Args: - id (string): Unsplash photo ID - response_format (string): Output as 'markdown' (default) or 'json' Returns: Statistics including downloads, views, likes with historical trends Examples: - Get stats: id="LBI7cgq3pbM"`, inputSchema: GetPhotoStatisticsSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, // Stats change over time openWorldHint: true } }, async (params: z.infer<typeof GetPhotoStatisticsSchema>) => { try { const stats = await client.makeRequest<PhotoStatistics>( `/photos/${params.id}/statistics` ); if (params.response_format === ResponseFormat.JSON) { return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }], structuredContent: stats }; } const markdown = formatStatisticsMarkdown(stats); return { content: [{ type: "text", text: markdown }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error fetching statistics: ${errorMessage}` }], isError: true }; } } ); // Track download server.registerTool( "unsplash_track_download", { title: "Track Photo Download", description: `Track a photo download (required by Unsplash API guidelines when downloading photos). When you download a photo programmatically, you must call this tool to properly attribute the download to the photographer. This helps photographers understand how their work is being used. Args: - id (string): Unsplash photo ID being downloaded Returns: Confirmation that download was tracked Examples: - Track download: id="LBI7cgq3pbM" IMPORTANT: Always call this before downloading/using a photo programmatically.`, inputSchema: DownloadPhotoSchema, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params: z.infer<typeof DownloadPhotoSchema>) => { try { // First get the photo to get the download_location const photo = await client.makeRequest<UnsplashPhoto>(`/photos/${params.id}`); // Track the download await client.trackDownload(photo.links.download_location); return { content: [{ type: "text", text: `Download tracked for photo ${params.id}. You can download the photo from: ${photo.urls.regular}` }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error tracking download: ${errorMessage}` }], isError: true }; } } ); }

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/mjordi/unsplash-mcp-server'

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