#
tokens: 11002/50000 3/3 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── go.mod
├── go.sum
├── main.go
├── Makefile
├── README.md
└── screenshots
    └── chatwise.webp
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Binaries for programs and plugins
 2 | *.exe
 3 | *.exe~
 4 | *.dll
 5 | *.so
 6 | *.dylib
 7 | 
 8 | # Binary output directories
 9 | bin/
10 | dist/
11 | .build/
12 | 
13 | # Test binary, built with `go test -c`
14 | *.test
15 | 
16 | # Output of the go coverage tool
17 | *.out
18 | 
19 | # Go workspace file
20 | go.work
21 | 
22 | # Dependency directories (remove the comment below if you want to ignore vendor/)
23 | # vendor/
24 | 
25 | # IDE-specific files
26 | .idea/
27 | .vscode/
28 | *.swp
29 | *.swo
30 | *~
31 | 
32 | # OS-specific files
33 | .DS_Store
34 | .DS_Store?
35 | ._*
36 | .Spotlight-V100
37 | .Trashes
38 | ehthumbs.db
39 | Thumbs.db
40 | 
41 | # Project specific
42 | *.json       # Ignore JSON files (like memory.json)
43 | !sample.json # But keep sample.json if exists
44 | 
45 | # Debug logs
46 | *.log
47 | 
48 | # Release assets
49 | *.zip
50 | *.tar.gz
51 | 
52 | # ai rules
53 | .rules
54 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Unsplash MCP Server
  2 | 
  3 | > A Model Context Protocol server that provides Unsplash photo search and retrieval capabilities. This server enables LLMs to search, retrieve, and get random photos from Unsplash's extensive collection. This is a Go implementation that offers tools like `search_photos`, `get_photo`, and `random_photo`.
  4 | 
  5 | ![Go Platform](https://img.shields.io/badge/platform-Go-00ADD8)
  6 | ![License](https://img.shields.io/badge/license-MIT-blue)
  7 | 
  8 | ## ✨ Features
  9 | 
 10 | * **Advanced Image Search**: Search Unsplash's extensive photo library with filters for:
 11 |   * Keyword relevance
 12 |   * Color schemes
 13 |   * Orientation options
 14 |   * Custom sorting and pagination
 15 | * **Detailed Photo Information**: Get comprehensive details about specific photos including EXIF data, location, and photographer information
 16 | * **Random Photo Selection**: Get random photos with flexible filtering options
 17 | * **Multiple Connection Modes**:
 18 |   * Standard I/O mode for direct integration with LLMs
 19 |   * Server-Sent Events (SSE) mode for web-based connections
 20 | 
 21 | ## Available Tools
 22 | 
 23 | * `search_photos` - Search for photos on Unsplash
 24 |   * `query` (string, required): Search keyword
 25 |   * `page` (number, optional): Page number (1-based), default: 1
 26 |   * `per_page` (number, optional): Results per page (1-30), default: 10
 27 |   * `order_by` (string, optional): Sort method (relevant or latest), default: "relevant"
 28 |   * `color` (string, optional): Color filter (black_and_white, black, white, yellow, orange, red, purple, magenta, green, teal, blue)
 29 |   * `orientation` (string, optional): Orientation filter (landscape, portrait, squarish)
 30 | 
 31 | * `get_photo` - Get detailed information about a specific photo
 32 |   * `photoId` (string, required): The photo ID to retrieve
 33 | 
 34 | * `random_photo` - Get one or more random photos
 35 |   * `count` (number, optional): The number of photos to return (Default: 1; Max: 30)
 36 |   * `collections` (string, optional): Public collection ID('s) to filter selection. If multiple, comma-separated
 37 |   * `topics` (string, optional): Public topic ID('s) to filter selection. If multiple, comma-separated
 38 |   * `username` (string, optional): Limit selection to a specific user
 39 |   * `query` (string, optional): Limit selection to photos matching a search term
 40 |   * `orientation` (string, optional): Filter by photo orientation. Valid values: landscape, portrait, squarish
 41 |   * `content_filter` (string, optional): Limit results by content safety. Valid values: low, high
 42 |   * `featured` (boolean, optional): Limit selection to featured photos
 43 | 
 44 | ## Installation
 45 | 
 46 | ### Option 1: Download Pre-built Binary
 47 | 
 48 | Download the latest pre-built binary for your platform from the [GitHub Releases](https://github.com/okooo5km/unsplash-mcp-server-go/releases/latest) page and follow the installation instructions below.
 49 | 
 50 | <details>
 51 | <summary><b>macOS Installation</b></summary>
 52 | 
 53 | #### macOS with Apple Silicon (M1/M2/M3):
 54 | ```bash
 55 | # Download the arm64 version
 56 | curl -L https://github.com/okooo5km/unsplash-mcp-server-go/releases/latest/download/unsplash-mcp-server-macos-arm64.zip -o unsplash-mcp-server.zip
 57 | unzip unsplash-mcp-server.zip
 58 | chmod +x unsplash-mcp-server
 59 | 
 60 | # Remove quarantine attribute to avoid security warnings
 61 | xattr -d com.apple.quarantine unsplash-mcp-server
 62 | 
 63 | # Install to your local bin directory
 64 | mkdir -p ~/.local/bin
 65 | mv unsplash-mcp-server ~/.local/bin/
 66 | rm unsplash-mcp-server.zip
 67 | ```
 68 | 
 69 | #### macOS with Intel Processor:
 70 | ```bash
 71 | # Download the x86_64 version
 72 | curl -L https://github.com/okooo5km/unsplash-mcp-server-go/releases/latest/download/unsplash-mcp-server-macos-x86_64.zip -o unsplash-mcp-server.zip
 73 | unzip unsplash-mcp-server.zip
 74 | chmod +x unsplash-mcp-server
 75 | 
 76 | # Remove quarantine attribute to avoid security warnings
 77 | xattr -d com.apple.quarantine unsplash-mcp-server
 78 | 
 79 | # Install to your local bin directory
 80 | mkdir -p ~/.local/bin
 81 | mv unsplash-mcp-server ~/.local/bin/
 82 | rm unsplash-mcp-server.zip
 83 | ```
 84 | 
 85 | #### macOS Universal Binary (works on both Apple Silicon and Intel):
 86 | ```bash
 87 | # Download the universal version
 88 | curl -L https://github.com/okooo5km/unsplash-mcp-server-go/releases/latest/download/unsplash-mcp-server-macos-universal.zip -o unsplash-mcp-server.zip
 89 | unzip unsplash-mcp-server.zip
 90 | chmod +x unsplash-mcp-server
 91 | 
 92 | # Remove quarantine attribute to avoid security warnings
 93 | xattr -d com.apple.quarantine unsplash-mcp-server
 94 | 
 95 | # Install to your local bin directory
 96 | mkdir -p ~/.local/bin
 97 | mv unsplash-mcp-server ~/.local/bin/
 98 | rm unsplash-mcp-server.zip
 99 | ```
100 | </details>
101 | 
102 | <details>
103 | <summary><b>Linux Installation</b></summary>
104 | 
105 | #### Linux on x86_64 (most common):
106 | ```bash
107 | # Download the amd64 version
108 | curl -L https://github.com/okooo5km/unsplash-mcp-server-go/releases/latest/download/unsplash-mcp-server-linux-amd64.tar.gz -o unsplash-mcp-server.tar.gz
109 | tar -xzf unsplash-mcp-server.tar.gz
110 | chmod +x unsplash-mcp-server
111 | 
112 | # Install to your local bin directory
113 | mkdir -p ~/.local/bin
114 | mv unsplash-mcp-server ~/.local/bin/
115 | rm unsplash-mcp-server.tar.gz
116 | ```
117 | 
118 | #### Linux on ARM64 (e.g., Raspberry Pi 4, AWS Graviton):
119 | ```bash
120 | # Download the arm64 version
121 | curl -L https://github.com/okooo5km/unsplash-mcp-server-go/releases/latest/download/unsplash-mcp-server-linux-arm64.tar.gz -o unsplash-mcp-server.tar.gz
122 | tar -xzf unsplash-mcp-server.tar.gz
123 | chmod +x unsplash-mcp-server
124 | 
125 | # Install to your local bin directory
126 | mkdir -p ~/.local/bin
127 | mv unsplash-mcp-server ~/.local/bin/
128 | rm unsplash-mcp-server.tar.gz
129 | ```
130 | </details>
131 | 
132 | <details>
133 | <summary><b>Windows Installation</b></summary>
134 | 
135 | #### Windows on x86_64 (most common):
136 | - Download the [Windows AMD64 version](https://github.com/okooo5km/unsplash-mcp-server-go/releases/latest/download/unsplash-mcp-server-windows-amd64.zip)
137 | - Extract the ZIP file
138 | - Move the `unsplash-mcp-server.exe` to a location in your PATH
139 | 
140 | #### Windows on ARM64 (e.g., Windows on ARM devices):
141 | - Download the [Windows ARM64 version](https://github.com/okooo5km/unsplash-mcp-server-go/releases/latest/download/unsplash-mcp-server-windows-arm64.zip)
142 | - Extract the ZIP file
143 | - Move the `unsplash-mcp-server.exe` to a location in your PATH
144 | </details>
145 | 
146 | Make sure the installation directory is in your PATH:
147 | 
148 | - **macOS/Linux**: Add `export PATH="$HOME/.local/bin:$PATH"` to your shell configuration file (`.bashrc`, `.zshrc`, etc.)
149 | - **Windows**: Add the directory to your system PATH through the System Properties > Environment Variables dialog
150 | 
151 | ### Option 2: Build from Source
152 | 
153 | 1. Clone the repository:
154 | 
155 |    ```bash
156 |    git clone https://github.com/okooo5km/unsplash-mcp-server-go.git
157 |    cd unsplash-mcp-server-go
158 |    ```
159 | 
160 | 2. Build the project:
161 | 
162 |    **Using Make (recommended):**
163 |    ```bash
164 |    # Build for your current platform
165 |    make
166 | 
167 |    # Or build for a specific platform
168 |    make build-darwin-universal    # macOS Universal Binary
169 |    make build-darwin-arm64        # macOS Apple Silicon
170 |    make build-darwin-amd64        # macOS Intel
171 |    make build-linux-amd64         # Linux x86_64
172 |    make build-linux-arm64         # Linux ARM64
173 |    make build-windows-amd64       # Windows x86_64
174 | 
175 |    # Or build for all platforms at once
176 |    make build-all
177 | 
178 |    # Create distribution packages for all platforms
179 |    make dist
180 |    ```
181 | 
182 |    The binaries will be placed in the `.build` directory.
183 | 
184 |    **Using Go directly:**
185 |    ```bash
186 |    go build -o unsplash-mcp-server
187 |    ```
188 | 
189 | 3. Install the binary:
190 | 
191 |    ```bash
192 |    # Install to user directory (recommended, no sudo required)
193 |    mkdir -p ~/.local/bin
194 |    cp unsplash-mcp-server ~/.local/bin/
195 |    ```
196 | 
197 |    Make sure `~/.local/bin` is in your PATH by adding to your shell configuration file:
198 | 
199 |    ```bash
200 |    echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc  # or ~/.bashrc
201 |    source ~/.zshrc  # or source ~/.bashrc
202 |    ```
203 | 
204 | ## Configuration
205 | 
206 | ### Environment Variables
207 | 
208 | The server requires an Unsplash API access key to function. Set it in your environment:
209 | 
210 | ```bash
211 | export UNSPLASH_ACCESS_KEY="your-access-key-here"
212 | ```
213 | 
214 | * Unsplash API Access Key (register at [Unsplash Developers Portal](https://unsplash.com/developers))
215 | 
216 | ### Obtain an Unsplash API Access Key
217 | 
218 | 1. Go to the [Unsplash Developers Portal](https://unsplash.com/developers)
219 | 2. Sign up or log in to your Unsplash account
220 | 3. Register a new application
221 | 4. Accept the API use and guidelines
222 | 5. Fill in your application details (name, description, etc.)
223 | 6. Once registered, you'll receive your Access Key (also called Client ID)
224 | 
225 | ### Configure for Claude.app
226 | 
227 | Add to your Claude settings:
228 | 
229 | ```json
230 | "mcpServers": {
231 |   "unsplash": {
232 |     "command": "unsplash-mcp-server",
233 |     "env": {
234 |       "UNSPLASH_ACCESS_KEY": "your-access-key-here"
235 |     }
236 |   }
237 | }
238 | ```
239 | 
240 | ### Configure for Cursor
241 | 
242 | Add the following configuration to your Cursor editor's `settings.json`:
243 | 
244 | ```json
245 | {
246 |   "mcpServers": {
247 |     "unsplash": {
248 |       "command": "unsplash-mcp-server",
249 |       "env": {
250 |         "UNSPLASH_ACCESS_KEY": "your-access-key-here"
251 |       }
252 |     }
253 |   }
254 | }
255 | ```
256 | 
257 | ### Configure for Chatwise
258 | 
259 | ![](screenshots/chatwise.webp)
260 | 
261 | > **Note**: When adding environment variables in Chatwise, do not wrap the value in quotes (fixes #1)
262 | 
263 | ## Command Line Arguments
264 | 
265 | The server supports the following command line arguments:
266 | 
267 | * `-h, --help`: Display help information about the server, its usage, and available options
268 | * `-v, --version`: Display the version number of the unsplash-mcp-server
269 | * `-t, --transport <string>`: Transport type to use (default: "stdio")
270 |   * `stdio`: Standard input/output mode for direct integration with LLMs
271 |   * `sse`: Server-Sent Events mode for web-based connections
272 | * `-p, --port <int>`: Port to use when running in SSE mode (default: 8080)
273 | 
274 | Example usage:
275 | 
276 | ```bash
277 | # Display help information
278 | unsplash-mcp-server --help
279 | 
280 | # Display version information
281 | unsplash-mcp-server --version
282 | 
283 | # Start server with default settings (stdio mode)
284 | unsplash-mcp-server
285 | 
286 | # Start server in SSE mode on the default port (8080)
287 | unsplash-mcp-server --transport sse
288 | 
289 | # Start server in SSE mode on a custom port
290 | unsplash-mcp-server --transport sse --port 9090
291 | ```
292 | 
293 | When running in SSE mode, the server will be accessible via HTTP on the specified port, allowing web-based clients to connect. In stdio mode (default), the server communicates through standard input/output, which is ideal for direct integration with LLM applications.
294 | 
295 | ## Example System Prompt
296 | 
297 | You can use the following system prompt to help Claude utilize the unsplash-mcp-server effectively:
298 | 
299 | ```
300 | You have access to Unsplash photo search tools through MCP. You can:
301 | 
302 | 1. Search for photos using specific keywords, colors, or orientations:
303 |    - Use the search_photos tool to find images on any topic
304 |    - Filter by color or orientation as needed
305 |    - You can page through results and control how many appear per page
306 | 
307 | 2. Get detailed information about specific photos:
308 |    - Use get_photo with a photo ID to retrieve comprehensive details
309 |    - This includes EXIF data, location info, and photographer details
310 | 
311 | 3. Fetch random photos with customizable filters:
312 |    - Use random_photo tool to get surprise images based on criteria
313 |    - Filter random selections by topic, collection, username, etc.
314 | 
315 | When the user asks for images, use these tools to find relevant Unsplash photos.
316 | Include photo URLs in your responses so users can view the images.
317 | ```
318 | 
319 | ## Development Requirements
320 | 
321 | * Go 1.20 or later
322 | * Unsplash API access key
323 | * MCP Go SDK 0.19.0 or later
324 | 
325 | ## Usage Examples
326 | 
327 | ### Searching for Photos
328 | 
329 | ```json
330 | {
331 |   "query": "mountain landscape",
332 |   "per_page": 5,
333 |   "color": "blue",
334 |   "orientation": "landscape"
335 | }
336 | ```
337 | 
338 | ### Getting Photo Details
339 | 
340 | ```json
341 | {
342 |   "photoId": "Dwu85P9SOIk"
343 | }
344 | ```
345 | 
346 | ### Getting Random Photos
347 | 
348 | ```json
349 | {
350 |   "count": 3,
351 |   "query": "coffee",
352 |   "orientation": "portrait"
353 | }
354 | ```
355 | 
356 | ## Version History
357 | 
358 | See GitHub Releases for version history and changelog.
359 | 
360 | ### ☕️ Support the Project
361 | 
362 | If you find unsplash-mcp-server useful, please consider supporting its development:
363 | 
364 | * ⭐️ Star the project on GitHub
365 | * 🐛 Report bugs or suggest features in the issue tracker
366 | * 🔄 Submit pull requests to help improve the code
367 | * 💝 Support via:
368 | 
369 | <p align="center">
370 |   <a href="https://buymeacoffee.com/okooo5km">
371 |     <img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=okooo5km&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" style="border-radius: 8px;" />
372 |   </a>
373 | </p>
374 | 
375 | ## License
376 | 
377 | unsplash-mcp-server-go is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
378 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"errors"
  7 | 	"flag"
  8 | 	"fmt"
  9 | 	"io"
 10 | 	"log"
 11 | 	"net/http"
 12 | 	"net/url"
 13 | 	"os"
 14 | 	"strconv"
 15 | 	"strings"
 16 | 
 17 | 	"github.com/mark3labs/mcp-go/mcp"
 18 | 	"github.com/mark3labs/mcp-go/server"
 19 | )
 20 | 
 21 | // Version information
 22 | const (
 23 | 	version = "0.2.0" // Initial Go version
 24 | 	appName = "unsplash-mcp-server"
 25 | )
 26 | 
 27 | // --- Unsplash API Client ---
 28 | 
 29 | const unsplashAPIBaseURL = "https://api.unsplash.com"
 30 | 
 31 | type UnsplashClient struct {
 32 | 	accessKey string
 33 | 	client    *http.Client
 34 | }
 35 | 
 36 | func NewUnsplashClient(accessKey string) (*UnsplashClient, error) {
 37 | 	if accessKey == "" {
 38 | 		return nil, errors.New("missing UNSPLASH_ACCESS_KEY environment variable")
 39 | 	}
 40 | 	return &UnsplashClient{
 41 | 		accessKey: accessKey,
 42 | 		client:    &http.Client{},
 43 | 	}, nil
 44 | }
 45 | 
 46 | func (c *UnsplashClient) makeAPIRequest(ctx context.Context, method, endpoint string, params url.Values) ([]byte, error) {
 47 | 	fullURL := fmt.Sprintf("%s%s", unsplashAPIBaseURL, endpoint)
 48 | 	if params != nil {
 49 | 		fullURL = fmt.Sprintf("%s?%s", fullURL, params.Encode())
 50 | 	}
 51 | 
 52 | 	req, err := http.NewRequestWithContext(ctx, method, fullURL, nil)
 53 | 	if err != nil {
 54 | 		return nil, fmt.Errorf("failed to create request: %w", err)
 55 | 	}
 56 | 
 57 | 	req.Header.Set("Accept-Version", "v1")
 58 | 	req.Header.Set("Authorization", fmt.Sprintf("Client-ID %s", c.accessKey))
 59 | 	req.Header.Set("User-Agent", fmt.Sprintf("%s/%s", appName, version))
 60 | 
 61 | 	log.Printf("Making API request to: %s", fullURL) // Basic logging
 62 | 
 63 | 	resp, err := c.client.Do(req)
 64 | 	if err != nil {
 65 | 		return nil, fmt.Errorf("failed to perform request: %w", err)
 66 | 	}
 67 | 	defer resp.Body.Close()
 68 | 
 69 | 	body, err := io.ReadAll(resp.Body)
 70 | 	if err != nil {
 71 | 		return nil, fmt.Errorf("failed to read response body: %w", err)
 72 | 	}
 73 | 
 74 | 	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
 75 | 		log.Printf("API Error: Status %d, Body: %s", resp.StatusCode, string(body))
 76 | 		return nil, fmt.Errorf("unsplash API error: status code %d", resp.StatusCode)
 77 | 	}
 78 | 
 79 | 	return body, nil
 80 | }
 81 | 
 82 | // --- Unsplash Structs ---
 83 | 
 84 | type UnsplashPhoto struct {
 85 | 	ID             string            `json:"id"`
 86 | 	Description    string            `json:"description"`
 87 | 	AltDescription string            `json:"alt_description"`
 88 | 	URLs           map[string]string `json:"urls"`
 89 | 	Width          int               `json:"width"`
 90 | 	Height         int               `json:"height"`
 91 | 	Likes          int               `json:"likes"`
 92 | 	Downloads      *int              `json:"downloads"` // Pointer for optional field
 93 | 	Location       *Location         `json:"location"`
 94 | 	Exif           *Exif             `json:"exif"`
 95 | 	User           *User             `json:"user"`
 96 | 	Tags           []Tag             `json:"tags"`
 97 | }
 98 | 
 99 | type Location struct {
100 | 	Name    string `json:"name"`
101 | 	City    string `json:"city"`
102 | 	Country string `json:"country"`
103 | }
104 | 
105 | type Exif struct {
106 | 	Make         string `json:"make"`
107 | 	Model        string `json:"model"`
108 | 	ExposureTime string `json:"exposure_time"`
109 | 	Aperture     string `json:"aperture"`
110 | 	FocalLength  string `json:"focal_length"`
111 | 	ISO          int    `json:"iso"`
112 | }
113 | 
114 | type User struct {
115 | 	Name         string `json:"name"`
116 | 	Username     string `json:"username"`
117 | 	PortfolioURL string `json:"portfolio_url"`
118 | }
119 | 
120 | type Tag struct {
121 | 	Title string `json:"title"`
122 | }
123 | 
124 | type SearchResponse struct {
125 | 	Results    []UnsplashPhoto `json:"results"`
126 | 	Total      int             `json:"total"`
127 | 	TotalPages int             `json:"total_pages"`
128 | }
129 | 
130 | // --- Tool Input Structs (for clarity, though we parse from map) ---
131 | 
132 | type SearchPhotosInput struct {
133 | 	Query       string
134 | 	Page        int
135 | 	PerPage     int
136 | 	OrderBy     string
137 | 	Color       string
138 | 	Orientation string
139 | }
140 | 
141 | type GetPhotoInput struct {
142 | 	PhotoID string
143 | }
144 | 
145 | type RandomPhotoInput struct {
146 | 	Count         int
147 | 	Collections   string
148 | 	Topics        string
149 | 	Username      string
150 | 	Query         string
151 | 	Orientation   string
152 | 	ContentFilter string
153 | 	Featured      bool
154 | }
155 | 
156 | // --- Tool Implementations ---
157 | 
158 | func createSearchPhotosTool(client *UnsplashClient) (mcp.Tool, server.ToolHandlerFunc) {
159 | 	tool := mcp.NewTool("search_photos",
160 | 		mcp.WithDescription("Search for Unsplash photos"),
161 | 		mcp.WithString("query", mcp.Description("Search keyword"), mcp.Required()),
162 | 		mcp.WithString("page", mcp.Description("Page number (1-based)")),
163 | 		mcp.WithString("per_page", mcp.Description("Results per page (1-30)")),
164 | 		mcp.WithString("order_by", mcp.Description("Sort method (relevant or latest)"), mcp.Enum("relevant", "latest")),
165 | 		mcp.WithString("color", mcp.Description("Color filter (black_and_white, black, white, yellow, orange, red, purple, magenta, green, teal, blue)"), mcp.Enum("black_and_white", "black", "white", "yellow", "orange", "red", "purple", "magenta", "green", "teal", "blue")),
166 | 		mcp.WithString("orientation", mcp.Description("Orientation filter (landscape, portrait, squarish)"), mcp.Enum("landscape", "portrait", "squarish")),
167 | 	)
168 | 
169 | 	handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
170 | 		params := url.Values{}
171 | 		input := SearchPhotosInput{Page: 1, PerPage: 10, OrderBy: "relevant"} // Defaults
172 | 
173 | 		if q, ok := request.Params.Arguments["query"].(string); ok {
174 | 			input.Query = q
175 | 			params.Set("query", q)
176 | 		} else {
177 | 			return nil, errors.New("missing required parameter: query")
178 | 		}
179 | 
180 | 		if p, ok := request.Params.Arguments["page"].(string); ok {
181 | 			pageNum, err := strconv.Atoi(p)
182 | 			if err == nil {
183 | 				input.Page = pageNum
184 | 			}
185 | 		} else if p, ok := request.Params.Arguments["page"].(float64); ok {
186 | 			input.Page = int(p)
187 | 		}
188 | 		params.Set("page", strconv.Itoa(input.Page))
189 | 
190 | 		if pp, ok := request.Params.Arguments["per_page"].(string); ok {
191 | 			perPage, err := strconv.Atoi(pp)
192 | 			if err == nil {
193 | 				input.PerPage = min(max(perPage, 1), 30)
194 | 			}
195 | 		} else if pp, ok := request.Params.Arguments["per_page"].(float64); ok {
196 | 			input.PerPage = min(max(int(pp), 1), 30)
197 | 		}
198 | 		params.Set("per_page", strconv.Itoa(input.PerPage))
199 | 
200 | 		if ob, ok := request.Params.Arguments["order_by"].(string); ok {
201 | 			input.OrderBy = ob
202 | 			params.Set("order_by", ob)
203 | 		} else {
204 | 			params.Set("order_by", input.OrderBy) // Ensure default is set if missing
205 | 		}
206 | 
207 | 		if c, ok := request.Params.Arguments["color"].(string); ok && c != "" {
208 | 			input.Color = c
209 | 			params.Set("color", c)
210 | 		}
211 | 		if o, ok := request.Params.Arguments["orientation"].(string); ok && o != "" {
212 | 			input.Orientation = o
213 | 			params.Set("orientation", o)
214 | 		}
215 | 
216 | 		body, err := client.makeAPIRequest(ctx, "GET", "/search/photos", params)
217 | 		if err != nil {
218 | 			return nil, fmt.Errorf("failed to search photos: %w", err)
219 | 		}
220 | 
221 | 		var searchResponse SearchResponse
222 | 		if err := json.Unmarshal(body, &searchResponse); err != nil {
223 | 			return nil, fmt.Errorf("failed to decode search response: %w", err)
224 | 		}
225 | 
226 | 		var resultText strings.Builder
227 | 		resultText.WriteString(fmt.Sprintf("Found %d photos (Page %d/%d):\n\n", len(searchResponse.Results), input.Page, searchResponse.TotalPages))
228 | 		for _, photo := range searchResponse.Results {
229 | 			resultText.WriteString(formatPhotoSummary(&photo))
230 | 			resultText.WriteString("\n")
231 | 		}
232 | 
233 | 		return mcp.NewToolResultText(resultText.String()), nil
234 | 	}
235 | 
236 | 	return tool, handler
237 | }
238 | 
239 | func createGetPhotoTool(client *UnsplashClient) (mcp.Tool, server.ToolHandlerFunc) {
240 | 	tool := mcp.NewTool("get_photo",
241 | 		mcp.WithDescription("Get detailed information about a specific Unsplash photo"),
242 | 		mcp.WithString("photoId", mcp.Description("The photo ID to retrieve"), mcp.Required()),
243 | 	)
244 | 
245 | 	handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
246 | 		photoID, ok := request.Params.Arguments["photoId"].(string)
247 | 		if !ok || photoID == "" {
248 | 			return nil, errors.New("missing or empty required parameter: photoId")
249 | 		}
250 | 
251 | 		endpoint := fmt.Sprintf("/photos/%s", photoID)
252 | 		body, err := client.makeAPIRequest(ctx, "GET", endpoint, nil)
253 | 		if err != nil {
254 | 			return nil, fmt.Errorf("failed to get photo %s: %w", photoID, err)
255 | 		}
256 | 
257 | 		var photo UnsplashPhoto
258 | 		if err := json.Unmarshal(body, &photo); err != nil {
259 | 			// Try decoding as DetailedPhoto if needed, but UnsplashPhoto covers most fields now
260 | 			return nil, fmt.Errorf("failed to decode photo details: %w", err)
261 | 		}
262 | 
263 | 		return mcp.NewToolResultText(formatPhotoDetails(&photo)), nil
264 | 	}
265 | 
266 | 	return tool, handler
267 | }
268 | 
269 | func createRandomPhotoTool(client *UnsplashClient) (mcp.Tool, server.ToolHandlerFunc) {
270 | 	tool := mcp.NewTool("random_photo",
271 | 		mcp.WithDescription("Get one or more random photos from Unsplash"),
272 | 		mcp.WithString("count", mcp.Description("Number of photos (1-30)")),
273 | 		mcp.WithString("collections", mcp.Description("Comma-separated public collection IDs")),
274 | 		mcp.WithString("topics", mcp.Description("Comma-separated public topic IDs")),
275 | 		mcp.WithString("username", mcp.Description("Limit to a specific user's photos")),
276 | 		mcp.WithString("query", mcp.Description("Limit results to matching photos")),
277 | 		mcp.WithString("orientation", mcp.Description("Filter by orientation"), mcp.Enum("landscape", "portrait", "squarish")),
278 | 		mcp.WithString("content_filter", mcp.Description("Content safety filter"), mcp.Enum("low", "high")),
279 | 		mcp.WithString("featured", mcp.Description("Limit to featured photos")),
280 | 	)
281 | 
282 | 	handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
283 | 		params := url.Values{}
284 | 		count := 1 // Default
285 | 
286 | 		if c, ok := request.Params.Arguments["count"].(string); ok {
287 | 			countNum, err := strconv.Atoi(c)
288 | 			if err == nil {
289 | 				count = min(max(countNum, 1), 30)
290 | 			}
291 | 		} else if c, ok := request.Params.Arguments["count"].(float64); ok {
292 | 			count = min(max(int(c), 1), 30)
293 | 		}
294 | 		// Note: Don't set count param if it's 1, as the API endpoint changes behavior
295 | 		if count > 1 {
296 | 			params.Set("count", strconv.Itoa(count))
297 | 		}
298 | 
299 | 		if v, ok := request.Params.Arguments["collections"].(string); ok && v != "" {
300 | 			params.Set("collections", v)
301 | 		}
302 | 		if v, ok := request.Params.Arguments["topics"].(string); ok && v != "" {
303 | 			params.Set("topics", v)
304 | 		}
305 | 		if v, ok := request.Params.Arguments["username"].(string); ok && v != "" {
306 | 			params.Set("username", v)
307 | 		}
308 | 		if v, ok := request.Params.Arguments["query"].(string); ok && v != "" {
309 | 			params.Set("query", v)
310 | 		}
311 | 		if v, ok := request.Params.Arguments["orientation"].(string); ok && v != "" {
312 | 			params.Set("orientation", v)
313 | 		}
314 | 		if v, ok := request.Params.Arguments["content_filter"].(string); ok && v != "" {
315 | 			params.Set("content_filter", v)
316 | 		}
317 | 		if v, ok := request.Params.Arguments["featured"].(string); ok && v != "" {
318 | 			featured, err := strconv.ParseBool(v)
319 | 			if err == nil && featured {
320 | 				params.Set("featured", "true")
321 | 			}
322 | 		} else if v, ok := request.Params.Arguments["featured"].(bool); ok {
323 | 			params.Set("featured", strconv.FormatBool(v))
324 | 		}
325 | 
326 | 		body, err := client.makeAPIRequest(ctx, "GET", "/photos/random", params)
327 | 		if err != nil {
328 | 			return nil, fmt.Errorf("failed to get random photo(s): %w", err)
329 | 		}
330 | 
331 | 		var photos []UnsplashPhoto
332 | 		// According to the API documentation:
333 | 		// 1. If no count parameter is provided, it returns a single object
334 | 		// 2. If count parameter is provided (even if it's 1), it returns an array
335 | 		// First try to parse as an array, if that fails try to parse as a single object
336 | 		if err := json.Unmarshal(body, &photos); err != nil {
337 | 			// If parsing as array fails, try parsing as a single object (when no count parameter was provided)
338 | 			var singlePhoto UnsplashPhoto
339 | 			if err := json.Unmarshal(body, &singlePhoto); err != nil {
340 | 				return nil, fmt.Errorf("failed to decode random photo: %w", err)
341 | 			}
342 | 			photos = append(photos, singlePhoto)
343 | 		}
344 | 
345 | 		var resultText strings.Builder
346 | 		resultText.WriteString(fmt.Sprintf("Random Photos (%d):\n\n", len(photos)))
347 | 		for i, photo := range photos {
348 | 			resultText.WriteString(fmt.Sprintf("Photo %d:\n", i+1))
349 | 			resultText.WriteString(formatPhotoSummary(&photo)) // Use summary for random
350 | 			resultText.WriteString("\n")
351 | 		}
352 | 
353 | 		return mcp.NewToolResultText(resultText.String()), nil
354 | 	}
355 | 
356 | 	return tool, handler
357 | }
358 | 
359 | // --- Formatting Helpers ---
360 | 
361 | func formatPhotoSummary(photo *UnsplashPhoto) string {
362 | 	var sb strings.Builder
363 | 	sb.WriteString(fmt.Sprintf("- ID: %s\n", photo.ID))
364 | 	desc := photo.Description
365 | 	if desc == "" {
366 | 		desc = photo.AltDescription
367 | 	}
368 | 	if desc != "" {
369 | 		sb.WriteString(fmt.Sprintf("  Description: %s\n", desc))
370 | 	}
371 | 	sb.WriteString(fmt.Sprintf("  Size: %dx%d\n", photo.Width, photo.Height))
372 | 	sb.WriteString("  URLs:\n")
373 | 	// Prioritize common URLs if available
374 | 	urlsToShow := []string{"small", "regular", "full", "raw"}
375 | 	shown := make(map[string]bool)
376 | 	for _, size := range urlsToShow {
377 | 		if url, ok := photo.URLs[size]; ok {
378 | 			sb.WriteString(fmt.Sprintf("    %s: %s\n", size, url))
379 | 			shown[size] = true
380 | 		}
381 | 	}
382 | 	// Show others if not already shown
383 | 	for size, url := range photo.URLs {
384 | 		if !shown[size] {
385 | 			sb.WriteString(fmt.Sprintf("    %s: %s\n", size, url))
386 | 		}
387 | 	}
388 | 	return sb.String()
389 | }
390 | 
391 | func formatPhotoDetails(photo *UnsplashPhoto) string {
392 | 	var sb strings.Builder
393 | 	sb.WriteString("Photo Details:\n\n")
394 | 	sb.WriteString(fmt.Sprintf("- ID: %s\n", photo.ID))
395 | 	if photo.Description != "" {
396 | 		sb.WriteString(fmt.Sprintf("- Description: %s\n", photo.Description))
397 | 	}
398 | 	if photo.AltDescription != "" {
399 | 		sb.WriteString(fmt.Sprintf("- Alt Description: %s\n", photo.AltDescription))
400 | 	}
401 | 	sb.WriteString(fmt.Sprintf("- Size: %dx%d\n", photo.Width, photo.Height))
402 | 	sb.WriteString(fmt.Sprintf("- Likes: %d\n", photo.Likes))
403 | 	if photo.Downloads != nil {
404 | 		sb.WriteString(fmt.Sprintf("- Downloads: %d\n", *photo.Downloads))
405 | 	}
406 | 
407 | 	if photo.User != nil {
408 | 		sb.WriteString("\nPhotographer:\n")
409 | 		sb.WriteString(fmt.Sprintf("- Name: %s\n", photo.User.Name))
410 | 		sb.WriteString(fmt.Sprintf("- Username: @%s\n", photo.User.Username))
411 | 		if photo.User.PortfolioURL != "" {
412 | 			sb.WriteString(fmt.Sprintf("- Portfolio: %s\n", photo.User.PortfolioURL))
413 | 		}
414 | 	}
415 | 
416 | 	if photo.Location != nil && (photo.Location.Name != "" || photo.Location.City != "" || photo.Location.Country != "") {
417 | 		sb.WriteString("\nLocation:\n")
418 | 		if photo.Location.Name != "" {
419 | 			sb.WriteString(fmt.Sprintf("- Name: %s\n", photo.Location.Name))
420 | 		}
421 | 		if photo.Location.City != "" {
422 | 			sb.WriteString(fmt.Sprintf("- City: %s\n", photo.Location.City))
423 | 		}
424 | 		if photo.Location.Country != "" {
425 | 			sb.WriteString(fmt.Sprintf("- Country: %s\n", photo.Location.Country))
426 | 		}
427 | 	}
428 | 
429 | 	if photo.Exif != nil && (photo.Exif.Make != "" || photo.Exif.Model != "" || photo.Exif.ExposureTime != "" || photo.Exif.Aperture != "" || photo.Exif.FocalLength != "" || photo.Exif.ISO > 0) {
430 | 		sb.WriteString("\nCamera Info:\n")
431 | 		if photo.Exif.Make != "" {
432 | 			sb.WriteString(fmt.Sprintf("- Camera Make: %s\n", photo.Exif.Make))
433 | 		}
434 | 		if photo.Exif.Model != "" {
435 | 			sb.WriteString(fmt.Sprintf("- Camera Model: %s\n", photo.Exif.Model))
436 | 		}
437 | 		if photo.Exif.ExposureTime != "" {
438 | 			sb.WriteString(fmt.Sprintf("- Exposure Time: %s\n", photo.Exif.ExposureTime))
439 | 		}
440 | 		if photo.Exif.Aperture != "" {
441 | 			sb.WriteString(fmt.Sprintf("- Aperture: %s\n", photo.Exif.Aperture))
442 | 		}
443 | 		if photo.Exif.FocalLength != "" {
444 | 			sb.WriteString(fmt.Sprintf("- Focal Length: %s\n", photo.Exif.FocalLength))
445 | 		}
446 | 		if photo.Exif.ISO > 0 {
447 | 			sb.WriteString(fmt.Sprintf("- ISO: %d\n", photo.Exif.ISO))
448 | 		}
449 | 	}
450 | 
451 | 	sb.WriteString("\nURLs:\n")
452 | 	for size, url := range photo.URLs {
453 | 		sb.WriteString(fmt.Sprintf("- %s: %s\n", size, url))
454 | 	}
455 | 
456 | 	if len(photo.Tags) > 0 {
457 | 		sb.WriteString("\nTags:\n")
458 | 		for _, tag := range photo.Tags {
459 | 			sb.WriteString(fmt.Sprintf("- %s\n", tag.Title))
460 | 		}
461 | 	}
462 | 
463 | 	return sb.String()
464 | }
465 | 
466 | // --- Main Application Logic ---
467 | 
468 | // printVersion prints version information
469 | func printVersion() {
470 | 	fmt.Printf("%s version %s\n", appName, version)
471 | }
472 | 
473 | // printUsage prints a custom usage message
474 | func printUsage() {
475 | 	fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0])
476 | 	fmt.Fprintf(os.Stderr, "%s is a Model Context Protocol server that provides tools for accessing Unsplash photos.\n\n", appName)
477 | 	fmt.Fprintf(os.Stderr, "Requires the UNSPLASH_ACCESS_KEY environment variable to be set.\n\n")
478 | 	fmt.Fprintf(os.Stderr, "Options:\n")
479 | 	flag.PrintDefaults()
480 | }
481 | 
482 | func main() {
483 | 	var transport string
484 | 	var port int
485 | 	var showVersion bool
486 | 	var showHelp bool
487 | 
488 | 	// Override the default usage message
489 | 	flag.Usage = printUsage
490 | 
491 | 	// Define command-line flags
492 | 	flag.StringVar(&transport, "transport", "stdio", "Transport type (stdio or sse)")
493 | 	flag.StringVar(&transport, "t", "stdio", "Transport type (stdio or sse) (shorthand)")
494 | 	flag.IntVar(&port, "port", 8080, "Port for SSE transport")
495 | 	flag.IntVar(&port, "p", 8080, "Port for SSE transport (shorthand)")
496 | 	flag.BoolVar(&showVersion, "version", false, "Show version information and exit")
497 | 	flag.BoolVar(&showVersion, "v", false, "Show version information and exit (shorthand)")
498 | 	flag.BoolVar(&showHelp, "help", false, "Show this help message and exit")
499 | 	flag.BoolVar(&showHelp, "h", false, "Show this help message and exit (shorthand)")
500 | 
501 | 	flag.Parse()
502 | 
503 | 	// Handle version flag
504 | 	if showVersion {
505 | 		printVersion()
506 | 		os.Exit(0)
507 | 	}
508 | 
509 | 	// Handle help flag
510 | 	if showHelp {
511 | 		printUsage()
512 | 		os.Exit(0)
513 | 	}
514 | 
515 | 	// --- Get Unsplash Access Key ---
516 | 	accessKey := os.Getenv("UNSPLASH_ACCESS_KEY")
517 | 	if accessKey == "" {
518 | 		fmt.Fprintln(os.Stderr, "Error: UNSPLASH_ACCESS_KEY environment variable not set.")
519 | 		os.Exit(1)
520 | 	}
521 | 
522 | 	// --- Create Unsplash Client ---
523 | 	unsplashClient, err := NewUnsplashClient(accessKey)
524 | 	if err != nil {
525 | 		fmt.Fprintf(os.Stderr, "Error creating Unsplash client: %v", err)
526 | 		os.Exit(1)
527 | 	}
528 | 
529 | 	// --- Create MCP Server ---
530 | 	s := server.NewMCPServer(
531 | 		appName,
532 | 		version,
533 | 		server.WithResourceCapabilities(false, false), // No resource providers needed
534 | 		server.WithLogging(),                          // Enable basic MCP logging
535 | 	)
536 | 
537 | 	// --- Add Tools ---
538 | 	searchTool, searchHandler := createSearchPhotosTool(unsplashClient)
539 | 	s.AddTool(searchTool, searchHandler)
540 | 
541 | 	getPhotoTool, getPhotoHandler := createGetPhotoTool(unsplashClient)
542 | 	s.AddTool(getPhotoTool, getPhotoHandler)
543 | 
544 | 	randomPhotoTool, randomPhotoHandler := createRandomPhotoTool(unsplashClient)
545 | 	s.AddTool(randomPhotoTool, randomPhotoHandler)
546 | 
547 | 	// --- Start Server based on transport ---
548 | 	fmt.Fprintf(os.Stderr, "%s v%s started successfully\n", appName, version)
549 | 
550 | 	if transport == "stdio" {
551 | 		fmt.Fprintln(os.Stderr, "Unsplash MCP Server running on stdio")
552 | 		if err := server.ServeStdio(s); err != nil {
553 | 			fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
554 | 			os.Exit(1) // Exit if server stops with error
555 | 		}
556 | 	} else if transport == "sse" {
557 | 		fmt.Fprintf(os.Stderr, "Unsplash MCP Server running on SSE mode at port %d\n", port)
558 | 		sseServer := server.NewSSEServer(s, server.WithBaseURL(fmt.Sprintf("http://localhost:%d", port)))
559 | 		log.Printf("Server started listening on :%d\n", port)
560 | 		if err := sseServer.Start(fmt.Sprintf(":%d", port)); err != nil {
561 | 			log.Fatalf("Failed to start server: %v", err)
562 | 		}
563 | 	} else {
564 | 		fmt.Fprintf(os.Stderr, "Error: Invalid transport type '%s'. Supported types: stdio, sse\n", transport)
565 | 		os.Exit(1)
566 | 	}
567 | 
568 | 	fmt.Fprintln(os.Stderr, "Unsplash MCP Server finished.")
569 | }
570 | 
```