# 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 |
```