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

```
├── .gitattributes
├── .github
│   └── workflows
│       └── publish.yml
├── .gitignore
├── .npmignore
├── code.md
├── glama.json
├── LICENSE
├── package-lock.json
├── package.json
├── product.md
├── README_zh.md
├── README.md
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

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

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
```

--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------

```
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 | 
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
 1 | # Source files
 2 | src/
 3 | *.ts
 4 | tsconfig.json
 5 | 
 6 | # Development files
 7 | .gitignore
 8 | .npmignore
 9 | 
10 | # Documentation (keep README files)
11 | # README.md
12 | # README_zh.md
13 | 
14 | # Development dependencies
15 | node_modules/
16 | npm-debug.log*
17 | yarn-debug.log*
18 | yarn-error.log*
19 | 
20 | # IDE files
21 | .vscode/
22 | .idea/
23 | *.swp
24 | *.swo
25 | 
26 | # OS files
27 | .DS_Store
28 | Thumbs.db
29 | 
30 | # Test files
31 | test/
32 | tests/
33 | *.test.js
34 | *.spec.js
35 | 
36 | # Coverage
37 | coverage/
38 | .nyc_output/
39 | 
40 | # Logs
41 | logs/
42 | *.log
```

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

```markdown
  1 | # pixabay-mcp MCP Server
  2 | 
  3 | [中文版](README_zh.md)
  4 | 
  5 | A Model Context Protocol (MCP) server for Pixabay image and video search with structured results & runtime validation.
  6 | 
  7 | <a href="https://glama.ai/mcp/servers/@zym9863/pixabay-mcp">
  8 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/@zym9863/pixabay-mcp/badge" alt="Pixabay Server MCP server" />
  9 | </a>
 10 | 
 11 | This TypeScript MCP server exposes Pixabay search tools over stdio so AI assistants / agents can retrieve media safely and reliably.
 12 | 
 13 | Highlights:
 14 | - Image & video search tools (Pixabay official API)
 15 | - Runtime argument validation (enums, ranges, semantic checks)
 16 | - Consistent error logging without leaking sensitive keys
 17 | - Planned structured JSON payloads for easier downstream automation (see Roadmap)
 18 | 
 19 | ## Features
 20 | 
 21 | ### Tools
 22 | `search_pixabay_images`
 23 |   - Required: `query` (string)
 24 |   - Optional: `image_type` (all|photo|illustration|vector), `orientation` (all|horizontal|vertical), `per_page` (3-200)
 25 |   - Returns: human-readable text block (current) + (planned) structured JSON array of hits
 26 | 
 27 | `search_pixabay_videos`
 28 |   - Required: `query`
 29 |   - Optional: `video_type` (all|film|animation), `orientation`, `per_page` (3-200), `min_duration`, `max_duration`
 30 |   - Returns: human-readable text block + (planned) structured JSON with duration & URLs
 31 | 
 32 | ### Configuration
 33 | Environment variables:
 34 | | Name | Required | Default | Description |
 35 | | ---- | -------- | ------- | ----------- |
 36 | | `PIXABAY_API_KEY` | Yes | - | Your Pixabay API key (images & videos) |
 37 | | `PIXABAY_TIMEOUT_MS` | No | 10000 (planned) | Request timeout once feature lands |
 38 | | `PIXABAY_RETRY` | No | 0 (planned) | Number of retry attempts for transient network errors |
 39 | 
 40 | Notes:
 41 | - Safe search is enabled by default.
 42 | - Keys are never echoed back in structured errors or logs.
 43 | 
 44 | ## Usage Examples
 45 | 
 46 | Current (text only response excerpt):
 47 | ```
 48 | Found 120 images for "cat":
 49 | - cat, pet, animal (User: Alice): https://.../medium1.jpg
 50 | - kitten, cute (User: Bob): https://.../medium2.jpg
 51 | ```
 52 | 
 53 | Planned structured result (Roadmap v0.4+):
 54 | ```jsonc
 55 | {
 56 |   "content": [
 57 |     { "type": "text", "text": "Found 120 images for \"cat\":\n- ..." },
 58 |     {
 59 |       "type": "json",
 60 |       "data": {
 61 |         "query": "cat",
 62 |         "totalHits": 120,
 63 |         "page": 1,
 64 |         "perPage": 20,
 65 |         "hits": [
 66 |           { "id": 123, "tags": ["cat","animal"], "user": "Alice", "previewURL": "...", "webformatURL": "...", "largeImageURL": "..." }
 67 |         ]
 68 |       }
 69 |     }
 70 |   ]
 71 | }
 72 | ```
 73 | 
 74 | Error response (planned shape):
 75 | ```json
 76 | {
 77 |   "content": [{ "type": "text", "text": "Pixabay API error: 400 ..." }],
 78 |   "isError": true,
 79 |   "metadata": { "status": 400, "code": "UPSTREAM_BAD_REQUEST", "hint": "Check API key or parameters" }
 80 | }
 81 | ```
 82 | 
 83 | ## Development
 84 | 
 85 | Install dependencies:
 86 | ```bash
 87 | npm install
 88 | ```
 89 | 
 90 | Build the server:
 91 | ```bash
 92 | npm run build
 93 | ```
 94 | 
 95 | Watch mode:
 96 | ```bash
 97 | npm run watch
 98 | ```
 99 | 
100 | ## Installation
101 | 
102 | ### Option 1: Using npx (Recommended)
103 | 
104 | Add this to your Claude Desktop configuration:
105 | 
106 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
107 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
108 | 
109 | ```json
110 | {
111 |   "mcpServers": {
112 |     "pixabay-mcp": {
113 |       "command": "npx",
114 |       "args": ["pixabay-mcp@latest"],
115 |       "env": {
116 |         "PIXABAY_API_KEY": "your_api_key_here"
117 |       }
118 |     }
119 |   }
120 | }
121 | ```
122 | 
123 | ### Option 2: Local Installation
124 | 
125 | 1. Clone and build the project:
126 | 
127 | ```bash
128 | git clone https://github.com/zym9863/pixabay-mcp.git
129 | cd pixabay-mcp
130 | npm install
131 | npm run build
132 | ```
133 | 
134 | 2. Add the server config:
135 | 
136 | ```json
137 | {
138 |   "mcpServers": {
139 |     "pixabay-mcp": {
140 |       "command": "/path/to/pixabay-mcp/build/index.js",
141 |       "env": {
142 |         "PIXABAY_API_KEY": "your_api_key_here"
143 |       }
144 |     }
145 |   }
146 | }
147 | ```
148 | 
149 | ### API Key Setup
150 | 
151 | Get your Pixabay API key from [https://pixabay.com/api/docs/](https://pixabay.com/api/docs/) and set it in the configuration above. The same key grants access to both image and video endpoints.
152 | 
153 | ### Debugging
154 | 
155 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
156 | 
157 | ```bash
158 | npm run inspector
159 | ```
160 | 
161 | The Inspector will provide a URL to access debugging tools in your browser.
162 | 
163 | ## Roadmap (Condensed)
164 | | Version | Focus | Key Items |
165 | | ------- | ----- | --------- |
166 | | v0.4 | Structured & Reliability | JSON payload, timeout, structured errors |
167 | | v0.5 | UX & Pagination | page/order params, limited retry, modular refactor, tests |
168 | | v0.6 | Multi-source Exploration | Evaluate integrating Unsplash/Pexels abstraction |
169 | 
170 | See `product.md` for full backlog & prioritization.
171 | 
172 | ## Contributing
173 | Planned contributions welcome once tests & module split land (v0.5 target). Feel free to open issues for API shape / schema suggestions.
174 | 
175 | ## License
176 | MIT
177 | 
178 | ## Disclaimer
179 | This project is not affiliated with Pixabay. Respect Pixabay's Terms of Service and rate limits.
180 | 
```

--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |     "$schema": "https://glama.ai/mcp/schemas/server.json",
3 |     "maintainers": [
4 |     "zym9863"
5 |     ]
6 | }
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Publish package
 2 | 
 3 | on:
 4 |   push:
 5 |     tags:
 6 |       - "v*"
 7 |   workflow_dispatch:
 8 | 
 9 | jobs:
10 |   publish:
11 |     runs-on: ubuntu-latest
12 |     permissions:
13 |       contents: read
14 |       id-token: write
15 |     steps:
16 |       - name: Checkout repository
17 |         uses: actions/checkout@v4
18 | 
19 |       - name: Set up Node.js
20 |         uses: actions/setup-node@v4
21 |         with:
22 |           node-version: 20
23 |           registry-url: https://registry.npmjs.org/
24 |           cache: npm
25 | 
26 |       - name: Install dependencies
27 |         run: npm ci
28 | 
29 |       - name: Build project
30 |         run: npm run build
31 | 
32 |       - name: Publish to npm
33 |         env:
34 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
35 |         run: npm publish --access public
36 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "pixabay-mcp",
 3 |   "version": "0.3.0",
 4 |   "description": "A Model Context Protocol server for Pixabay image search",
 5 |   "author": "zym9863",
 6 |   "license": "MIT",
 7 |   "keywords": ["mcp", "pixabay", "image-search", "model-context-protocol"],
 8 |   "repository": {
 9 |     "type": "git",
10 |     "url": "https://github.com/zym9863/pixabay-mcp.git"
11 |   },
12 |   "homepage": "https://github.com/zym9863/pixabay-mcp#readme",
13 |   "bugs": {
14 |     "url": "https://github.com/zym9863/pixabay-mcp/issues"
15 |   },
16 |   "type": "module",
17 |   "bin": {
18 |     "pixabay-mcp": "./build/index.js"
19 |   },
20 |   "files": [
21 |     "build"
22 |   ],
23 |   "scripts": {
24 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
25 |     "prepare": "npm run build",
26 |     "watch": "tsc --watch",
27 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
28 |   },
29 |   "dependencies": {
30 |     "@modelcontextprotocol/sdk": "0.6.0",
31 |     "axios": "^1.8.4"
32 |   },
33 |   "devDependencies": {
34 |     "@types/node": "^20.11.24",
35 |     "typescript": "^5.3.3"
36 |   }
37 | }
38 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListToolsRequestSchema,
  8 |   McpError,
  9 |   ErrorCode,
 10 | } from "@modelcontextprotocol/sdk/types.js";
 11 | import axios from 'axios';
 12 | 
 13 | // Pixabay API Key from environment variable
 14 | const API_KEY = process.env.PIXABAY_API_KEY;
 15 | const PIXABAY_API_URL = 'https://pixabay.com/api/';
 16 | const PIXABAY_VIDEO_API_URL = 'https://pixabay.com/api/videos/';
 17 | const IMAGE_TYPE_OPTIONS = ["all", "photo", "illustration", "vector"];
 18 | const ORIENTATION_OPTIONS = ["all", "horizontal", "vertical"];
 19 | const VIDEO_TYPE_OPTIONS = ["all", "film", "animation"];
 20 | const MIN_PER_PAGE = 3;
 21 | const MAX_PER_PAGE = 200;
 22 | 
 23 | // Interface for Pixabay API response (simplified)
 24 | interface PixabayImage {
 25 |   id: number;
 26 |   pageURL: string;
 27 |   type: string;
 28 |   tags: string;
 29 |   previewURL: string;
 30 |   webformatURL: string;
 31 |   largeImageURL: string;
 32 |   views: number;
 33 |   downloads: number;
 34 |   likes: number;
 35 |   user: string;
 36 | }
 37 | 
 38 | interface PixabayResponse {
 39 |   total: number;
 40 |   totalHits: number;
 41 |   hits: PixabayImage[];
 42 | }
 43 | 
 44 | /**
 45 |  * 视频对象接口,包含视频的详细信息
 46 |  */
 47 | interface PixabayVideo {
 48 |   id: number;
 49 |   pageURL: string;
 50 |   type: string;
 51 |   tags: string;
 52 |   duration: number;
 53 |   views: number;
 54 |   downloads: number;
 55 |   likes: number;
 56 |   user: string;
 57 |   /**
 58 |    * 视频文件对象,包含不同分辨率的视频 URL
 59 |    */
 60 |   videos: {
 61 |     large?: {
 62 |       url: string;
 63 |       width: number;
 64 |       height: number;
 65 |       size: number;
 66 |     };
 67 |     medium?: {
 68 |       url: string;
 69 |       width: number;
 70 |       height: number;
 71 |       size: number;
 72 |     };
 73 |     small?: {
 74 |       url: string;
 75 |       width: number;
 76 |       height: number;
 77 |       size: number;
 78 |     };
 79 |     tiny?: {
 80 |       url: string;
 81 |       width: number;
 82 |       height: number;
 83 |       size: number;
 84 |     };
 85 |   };
 86 | }
 87 | 
 88 | /**
 89 |  * 视频搜索 API 响应接口
 90 |  */
 91 | interface PixabayVideoResponse {
 92 |   total: number;
 93 |   totalHits: number;
 94 |   hits: PixabayVideo[];
 95 | }
 96 | 
 97 | // Type guard for tool arguments
 98 | const isValidSearchArgs = (
 99 |   args: any
100 | ): args is { query: string; image_type?: string; orientation?: string; per_page?: number } =>
101 |   typeof args === 'object' &&
102 |   args !== null &&
103 |   typeof args.query === 'string' &&
104 |   (args.image_type === undefined || typeof args.image_type === 'string') &&
105 |   (args.orientation === undefined || typeof args.orientation === 'string') &&
106 |   (args.per_page === undefined || typeof args.per_page === 'number');
107 | 
108 | /**
109 |  * 视频搜索参数的类型守卫函数
110 |  */
111 | const isValidVideoSearchArgs = (
112 |   args: any
113 | ): args is { query: string; video_type?: string; orientation?: string; per_page?: number; min_duration?: number; max_duration?: number } =>
114 |   typeof args === 'object' &&
115 |   args !== null &&
116 |   typeof args.query === 'string' &&
117 |   (args.video_type === undefined || typeof args.video_type === 'string') &&
118 |   (args.orientation === undefined || typeof args.orientation === 'string') &&
119 |   (args.per_page === undefined || typeof args.per_page === 'number') &&
120 |   (args.min_duration === undefined || typeof args.min_duration === 'number') &&
121 |   (args.max_duration === undefined || typeof args.max_duration === 'number');
122 | 
123 | /**
124 |  * 验证图片检索参数,确保符合 Pixabay API 的要求。
125 |  */
126 | function assertValidImageSearchParams(args: { image_type?: string; orientation?: string; per_page?: number }): void {
127 |   const { image_type, orientation, per_page } = args;
128 | 
129 |   if (image_type !== undefined && !IMAGE_TYPE_OPTIONS.includes(image_type)) {
130 |     throw new McpError(
131 |       ErrorCode.InvalidParams,
132 |       `image_type 必须是 ${IMAGE_TYPE_OPTIONS.join(', ')} 之一。`
133 |     );
134 |   }
135 | 
136 |   if (orientation !== undefined && !ORIENTATION_OPTIONS.includes(orientation)) {
137 |     throw new McpError(
138 |       ErrorCode.InvalidParams,
139 |       `orientation 必须是 ${ORIENTATION_OPTIONS.join(', ')} 之一。`
140 |     );
141 |   }
142 | 
143 |   if (per_page !== undefined) {
144 |     if (!Number.isInteger(per_page)) {
145 |       throw new McpError(
146 |         ErrorCode.InvalidParams,
147 |         'per_page 必须是整数。'
148 |       );
149 |     }
150 |     if (per_page < MIN_PER_PAGE || per_page > MAX_PER_PAGE) {
151 |       throw new McpError(
152 |         ErrorCode.InvalidParams,
153 |         `per_page 需在 ${MIN_PER_PAGE}-${MAX_PER_PAGE} 范围内。`
154 |       );
155 |     }
156 |   }
157 | }
158 | 
159 | /**
160 |  * 验证视频检索参数,确保符合 Pixabay API 的要求。
161 |  */
162 | function assertValidVideoSearchParams(args: {
163 |   video_type?: string;
164 |   orientation?: string;
165 |   per_page?: number;
166 |   min_duration?: number;
167 |   max_duration?: number;
168 | }): void {
169 |   const { video_type, orientation, per_page, min_duration, max_duration } = args;
170 | 
171 |   if (video_type !== undefined && !VIDEO_TYPE_OPTIONS.includes(video_type)) {
172 |     throw new McpError(
173 |       ErrorCode.InvalidParams,
174 |       `video_type 必须是 ${VIDEO_TYPE_OPTIONS.join(', ')} 之一。`
175 |     );
176 |   }
177 | 
178 |   if (orientation !== undefined && !ORIENTATION_OPTIONS.includes(orientation)) {
179 |     throw new McpError(
180 |       ErrorCode.InvalidParams,
181 |       `orientation 必须是 ${ORIENTATION_OPTIONS.join(', ')} 之一。`
182 |     );
183 |   }
184 | 
185 |   if (per_page !== undefined) {
186 |     if (!Number.isInteger(per_page)) {
187 |       throw new McpError(
188 |         ErrorCode.InvalidParams,
189 |         'per_page 必须是整数。'
190 |       );
191 |     }
192 |     if (per_page < MIN_PER_PAGE || per_page > MAX_PER_PAGE) {
193 |       throw new McpError(
194 |         ErrorCode.InvalidParams,
195 |         `per_page 需在 ${MIN_PER_PAGE}-${MAX_PER_PAGE} 范围内。`
196 |       );
197 |     }
198 |   }
199 | 
200 |   if (min_duration !== undefined) {
201 |     if (!Number.isInteger(min_duration) || min_duration < 0) {
202 |       throw new McpError(
203 |         ErrorCode.InvalidParams,
204 |         'min_duration 必须是大于等于 0 的整数。'
205 |       );
206 |     }
207 |   }
208 | 
209 |   if (max_duration !== undefined) {
210 |     if (!Number.isInteger(max_duration) || max_duration < 0) {
211 |       throw new McpError(
212 |         ErrorCode.InvalidParams,
213 |         'max_duration 必须是大于等于 0 的整数。'
214 |       );
215 |     }
216 |   }
217 | 
218 |   if (
219 |     min_duration !== undefined &&
220 |     max_duration !== undefined &&
221 |     max_duration < min_duration
222 |   ) {
223 |     throw new McpError(
224 |       ErrorCode.InvalidParams,
225 |       'max_duration 需大于或等于 min_duration。'
226 |     );
227 |   }
228 | }
229 | 
230 | /**
231 |  * 输出 Pixabay API 错误日志,避免泄露敏感凭据信息。
232 |  */
233 | function logPixabayError(source: 'image' | 'video' | 'server', error: unknown): void {
234 |   if (axios.isAxiosError(error)) {
235 |     const responseData = error.response?.data as { message?: string } | undefined;
236 |     console.error(`[Pixabay ${source} error]`, {
237 |       status: error.response?.status,
238 |       statusText: error.response?.statusText,
239 |       code: error.code,
240 |       message: responseData?.message ?? error.message,
241 |     });
242 |     return;
243 |   }
244 | 
245 |   if (error instanceof Error) {
246 |     console.error(`[Pixabay ${source} error]`, {
247 |       message: error.message,
248 |     });
249 |     return;
250 |   }
251 | 
252 |   console.error(`[Pixabay ${source} error]`, {
253 |     error: String(error),
254 |   });
255 | }
256 | 
257 | /**
258 |  * Create an MCP server for Pixabay.
259 |  */
260 | const server = new Server(
261 |   {
262 |     name: "pixabay-mcp",
263 |     version: "0.3.0",
264 |   },
265 |   {
266 |     capabilities: {
267 |       tools: {},
268 |     },
269 |   }
270 | );
271 | 
272 | /**
273 |  * Handler that lists available tools.
274 |  * Exposes a "search_pixabay_images" tool.
275 |  */
276 | server.setRequestHandler(ListToolsRequestSchema, async () => {
277 |   return {
278 |     tools: [
279 |       {
280 |         name: "search_pixabay_images",
281 |         description: "Search for images on Pixabay",
282 |         inputSchema: {
283 |           type: "object",
284 |           properties: {
285 |             query: {
286 |               type: "string",
287 |               description: "Search query terms"
288 |             },
289 |             image_type: {
290 |               type: "string",
291 |               enum: ["all", "photo", "illustration", "vector"],
292 |               description: "Filter results by image type",
293 |               default: "all"
294 |             },
295 |             orientation: {
296 |               type: "string",
297 |               enum: ["all", "horizontal", "vertical"],
298 |               description: "Filter results by image orientation",
299 |               default: "all"
300 |             },
301 |             per_page: {
302 |               type: "number",
303 |               description: "Number of results per page (3-200)",
304 |               default: 20,
305 |               minimum: 3,
306 |               maximum: 200
307 |             }
308 |           },
309 |           required: ["query"]
310 |         }
311 |       },
312 |       {
313 |         name: "search_pixabay_videos",
314 |         description: "Search for videos on Pixabay",
315 |         inputSchema: {
316 |           type: "object",
317 |           properties: {
318 |             query: {
319 |               type: "string",
320 |               description: "Search query terms"
321 |             },
322 |             video_type: {
323 |               type: "string",
324 |               enum: ["all", "film", "animation"],
325 |               description: "Filter results by video type",
326 |               default: "all"
327 |             },
328 |             orientation: {
329 |               type: "string",
330 |               enum: ["all", "horizontal", "vertical"],
331 |               description: "Filter results by video orientation",
332 |               default: "all"
333 |             },
334 |             per_page: {
335 |               type: "number",
336 |               description: "Number of results per page (3-200)",
337 |               default: 20,
338 |               minimum: 3,
339 |               maximum: 200
340 |             },
341 |             min_duration: {
342 |               type: "number",
343 |               description: "Minimum video duration in seconds",
344 |               minimum: 0
345 |             },
346 |             max_duration: {
347 |               type: "number",
348 |               description: "Maximum video duration in seconds",
349 |               minimum: 0
350 |             }
351 |           },
352 |           required: ["query"]
353 |         }
354 |       }
355 |     ]
356 |   };
357 | });
358 | 
359 | /**
360 |  * Handler for the search_pixabay_images and search_pixabay_videos tools.
361 |  */
362 | interface ToolCallRequest {
363 |   params: {
364 |     name: string;
365 |     arguments?: Record<string, unknown>;
366 |   };
367 | }
368 | 
369 | server.setRequestHandler(CallToolRequestSchema, async (request: ToolCallRequest) => {
370 |   if (!['search_pixabay_images', 'search_pixabay_videos'].includes(request.params.name)) {
371 |     throw new McpError(
372 |       ErrorCode.MethodNotFound,
373 |       `Unknown tool: ${request.params.name}`
374 |     );
375 |   }
376 | 
377 |   if (!API_KEY) {
378 |     throw new McpError(
379 |       ErrorCode.InternalError,
380 |       'Pixabay API key (PIXABAY_API_KEY) is not configured in the environment.'
381 |     );
382 |   }
383 | 
384 |   // 处理图片搜索
385 |   if (request.params.name === 'search_pixabay_images') {
386 |     if (!isValidSearchArgs(request.params.arguments)) {
387 |       throw new McpError(
388 |         ErrorCode.InvalidParams,
389 |         'Invalid search arguments. "query" (string) is required.'
390 |       );
391 |     }
392 | 
393 |     assertValidImageSearchParams(request.params.arguments);
394 | 
395 |     const { query, image_type = 'all', orientation = 'all', per_page = 20 } = request.params.arguments;
396 | 
397 |     try {
398 |       const response = await axios.get<PixabayResponse>(PIXABAY_API_URL, {
399 |         params: {
400 |           key: API_KEY,
401 |           q: query,
402 |           image_type: image_type,
403 |           orientation: orientation,
404 |           per_page: per_page,
405 |           safesearch: true // Enable safe search by default
406 |         },
407 |       });
408 | 
409 |       if (response.data.hits.length === 0) {
410 |         return {
411 |           content: [{
412 |             type: "text",
413 |             text: `No images found for query: "${query}"`
414 |           }]
415 |         };
416 |       }
417 | 
418 |       // Format the results
419 |       const resultsText = response.data.hits.map((hit: PixabayImage) =>
420 |         `- ${hit.tags} (User: ${hit.user}): ${hit.webformatURL}`
421 |       ).join('\n');
422 | 
423 |       return {
424 |         content: [{
425 |           type: "text",
426 |           text: `Found ${response.data.totalHits} images for "${query}":\n${resultsText}`
427 |         }]
428 |       };
429 | 
430 |     } catch (error: unknown) {
431 |       let errorMessage = 'Failed to fetch images from Pixabay.';
432 |       if (axios.isAxiosError(error)) {
433 |         errorMessage = `Pixabay API error: ${error.response?.status} ${error.response?.data?.message || error.message}`;
434 |         // Pixabay might return 400 for invalid key, but doesn't give a clear message
435 |         if (error.response?.status === 400) {
436 |            errorMessage += '. Please check if the API key is valid.';
437 |         }
438 |       } else if (error instanceof Error) {
439 |         errorMessage = error.message;
440 |       }
441 |       logPixabayError('image', error);
442 |       return {
443 |         content: [{
444 |           type: "text",
445 |           text: errorMessage
446 |         }],
447 |         isError: true
448 |       };
449 |     }
450 |   }
451 | 
452 |   // 处理视频搜索
453 |   if (request.params.name === 'search_pixabay_videos') {
454 |     if (!isValidVideoSearchArgs(request.params.arguments)) {
455 |       throw new McpError(
456 |         ErrorCode.InvalidParams,
457 |         'Invalid video search arguments. "query" (string) is required.'
458 |       );
459 |     }
460 | 
461 |     assertValidVideoSearchParams(request.params.arguments);
462 | 
463 |     const {
464 |       query,
465 |       video_type = 'all',
466 |       orientation = 'all',
467 |       per_page = 20,
468 |       min_duration,
469 |       max_duration
470 |     } = request.params.arguments;
471 | 
472 |     try {
473 |       const params: any = {
474 |         key: API_KEY,
475 |         q: query,
476 |         video_type: video_type,
477 |         orientation: orientation,
478 |         per_page: per_page,
479 |         safesearch: true
480 |       };
481 | 
482 |       // 只有在提供了时长参数时才添加到请求中
483 |       if (min_duration !== undefined) {
484 |         params.min_duration = min_duration;
485 |       }
486 |       if (max_duration !== undefined) {
487 |         params.max_duration = max_duration;
488 |       }
489 | 
490 |       const response = await axios.get<PixabayVideoResponse>(PIXABAY_VIDEO_API_URL, {
491 |         params
492 |       });
493 | 
494 |       if (response.data.hits.length === 0) {
495 |         return {
496 |           content: [{
497 |             type: "text",
498 |             text: `No videos found for query: "${query}"`
499 |           }]
500 |         };
501 |       }
502 | 
503 |       // 格式化视频搜索结果
504 |       const resultsText = response.data.hits.map((hit: PixabayVideo) => {
505 |         const duration = Math.floor(hit.duration);
506 |         const videoUrl = hit.videos.medium?.url || hit.videos.small?.url || hit.videos.tiny?.url || 'No video URL available';
507 |         return `- ${hit.tags} (User: ${hit.user}, Duration: ${duration}s): ${videoUrl}`;
508 |       }).join('\n');
509 | 
510 |       return {
511 |         content: [{
512 |           type: "text",
513 |           text: `Found ${response.data.totalHits} videos for "${query}":\n${resultsText}`
514 |         }]
515 |       };
516 | 
517 |     } catch (error: unknown) {
518 |       let errorMessage = 'Failed to fetch videos from Pixabay.';
519 |       if (axios.isAxiosError(error)) {
520 |         errorMessage = `Pixabay Video API error: ${error.response?.status} ${error.response?.data?.message || error.message}`;
521 |         if (error.response?.status === 400) {
522 |            errorMessage += '. Please check if the API key is valid.';
523 |         }
524 |       } else if (error instanceof Error) {
525 |         errorMessage = error.message;
526 |       }
527 |       logPixabayError('video', error);
528 |       return {
529 |         content: [{
530 |           type: "text",
531 |           text: errorMessage
532 |         }],
533 |         isError: true
534 |       };
535 |     }
536 |   }
537 | 
538 |   // 这个不应该被执行到,但添加以确保类型安全
539 |   throw new McpError(
540 |     ErrorCode.MethodNotFound,
541 |     `Unhandled tool: ${request.params.name}`
542 |   );
543 | });
544 | 
545 | /**
546 |  * Start the server using stdio transport.
547 |  */
548 | async function main() {
549 |   const transport = new StdioServerTransport();
550 |   server.onerror = (error: Error) => logPixabayError('server', error); // Add basic error logging
551 |   process.on('SIGINT', async () => { // Graceful shutdown
552 |       await server.close();
553 |       process.exit(0);
554 |   });
555 |   await server.connect(transport);
556 |   console.error('Pixabay MCP server running on stdio'); // Log to stderr so it doesn't interfere with stdout JSON-RPC
557 | }
558 | 
559 | main().catch((error: unknown) => {
560 |   console.error("Server failed to start.");
561 |   logPixabayError('server', error);
562 |   process.exit(1);
563 | });
564 | 
```