pexels-mcp-server

pexels-mcp-server

An MCP server that enables AI agents to search, retrieve, and curate free stock photos and videos from Pexels, with tools, resources, and prompts for easy integration.

Category
访问服务器

README

pexels-mcp-server

CI License: MIT Python 3.10+ MCP

A Model Context Protocol (MCP) server that gives AI agents access to free stock photos and videos from Pexels. Plug it into claude.ai web, Claude Desktop, Claude Code, Cursor or any MCP-aware client and the model gains the three MCP primitives (tools, resources, prompts) over the Pexels REST surface.

Built around the MCP spec 2025-11-25 and Anthropic's Writing tools for agents guidance: strict Pydantic input schemas, structured tool output via structuredContent + outputSchema, isError=true on tool failure per SEP-1303, OAuth 2.1 + RFC 9728 + RFC 7591 DCR + PKCE for the HTTP transport.

What the agent gets

8 tools (model-controlled)

Tool Purpose
pexels_search_photos Search photos. Filters: orientation, size, color, locale, plus post-hoc min_width / min_height / aspect_ratio.
pexels_get_photo Fetch one photo by id.
pexels_search_videos Search videos. Same filters minus color.
pexels_get_video Fetch one video by id.
pexels_get_collection_media Read photos + videos in a Pexels collection.
pexels_get_curated_photos Pexels' editor-curated daily photo feed. Post-hoc dim/aspect filters.
pexels_get_popular_videos Trending video feed. Native min_width / min_height / min_duration / max_duration (Pexels-side), post-hoc aspect_ratio.
pexels_get_featured_collections Discover curated collection ids (metadata only — pipe an id into pexels_get_collection_media).

3 resources (app-controlled, URI templates)

URI template MIME Body
pexels://photo/{photo_id} application/json SinglePhotoResult
pexels://video/{video_id} application/json SingleVideoResult
pexels://collection/{collection_id} application/json CollectionMediaResult

A user pasting a pexels.com URL into a chat lets the host attach the content directly without the agent invoking a tool.

2 prompts (user-controlled, claude.ai connector menu)

Prompt Arguments Use case
find_hero_image topic, orientation?, brand_color?, aspect_ratio? Marketing hero with brand fit
find_broll topic, orientation?, resolution?, aspect_ratio? B-roll, reels, hero loops

Each prompt renders a short user-message brief that names the tool, the filters and the attribution requirement — the agent acts in one turn instead of asking the user for parameters.

Token economy

Every byte that goes onto the wire was audited. Cumulative gains vs the SDK defaults:

  • Tool descriptions trimmed to the minimum LLM-actionable signal (USE WHEN / DO NOT USE / filters / return shape).
  • Type docstrings removed from MediaSize, PhotoProjection, VideoProjection, FilterDiagnostics etc.: they leaked as description fields into every tool's $defs, duplicated across all tools that referenced them. Now Python comments only.
  • serverInfo.instructions reduced to one sentence (the attribution requirement); the tool list is already shipped by tools/list.
  • SDK patch (see _sdk_patches.py):
    • Forces model_dump(exclude_unset=True) so unset optional TypedDict fields don't leak as "field": null.
    • Replaces the SDK's duplicate-content behaviour: instead of shipping the payload twice (once as structuredContent, once as indented JSON in content[]), tools now ship the structured payload plus a 45-char marker in content[] pointing at it. Saves ~1500 tokens per tool call on a 15-photo search.

Numbers for a typical 15-photo search call:

content text structuredContent total
SDK default 7 100c (indented dup) 5 400c 12 500c (~3 100 tok)
This server 45c (marker) 5 400c 5 450c (~1 360 tok)

How the agent picks the best image

Pexels already ranks results by relevance. The tools just let the agent narrow the field in one shot:

  1. Frame query + filters — orientation for hero banners, aspect_ratio for fixed-frame (Instagram 1:1, Story 9:16, hero 16:9), min_width / min_height for hard pixel floors (~4000 for A4 print, ~1920 for hero), color for brand fit.
  2. Read alt text — pexels_search_photos returns up to 15 candidates by default with alt text, dimensions and photographer credit. The agent drops anything off-topic and returns the best image_url plus the mandatory photographer / photographer_url.

When a post-hoc filter (aspect_ratio etc.) wipes the page, the envelope carries a filter_diagnostics block telling the agent how to retry.

Deployment

Designed for one hosted HTTPS endpoint with OAuth 2.1 + RFC 9728. Stdio is supported for local power-user clients (Cursor, scripts).

Auth model — bring-your-own-key (BYOK) during the OAuth flow

The Python process is both the Resource Server (holding /mcp) and the Authorization Server. The MCP Python SDK mounts every well-known endpoint automatically: /.well-known/oauth-protected-resource (RFC 9728), /.well-known/oauth-authorization-server (RFC 8414), /authorize, /token, /register (RFC 7591 DCR), all with PKCE.

register_client rejects redirect_uri schemes that aren't https:// or http:// loopback (OAuth 2.1 phishing mitigation).

After the standard handshake, the server redirects the user's browser to /setup, a short HTML form asking for a Pexels API key. The user pastes their free key (from https://www.pexels.com/api/), the server validates it against api.pexels.com, then mints the OAuth code with the key bound to the soon-to-be-issued access token (30-day TTL). Every tool / resource call resolves the caller's key by Bearer-token lookup.

For per-request clients (Cursor stdio bridges, scripts), the server also accepts an X-Pexels-Api-Key HTTP header as a fallback.

Environment variables

Variable Required Description
TRANSPORT yes streamable-http or stdio (default).
MCP_SERVER_URL yes (HTTP) Public HTTPS URL of this service. No trailing slash.
MCP_ALLOWED_HOSTS no Comma-separated Host allowlist (DNS rebinding protection). Auto-set to MCP_SERVER_URL's hostname if unset.
MCP_RATE_LIMIT_PER_MINUTE no (60) Per-IP rate limit. /healthz, /readyz, OAuth metadata are exempt.
MCP_TRUSTED_PROXY_HOPS no (1) Proxies in front of the app (Koyeb LB = 1, Cloudflare-then-Koyeb = 2, no proxy = 0).
REDIS_URL no When set, OAuth state lives in Redis and survives restarts. Supports rediss:// (TLS).
MCP_ENCRYPTION_KEY yes if REDIS_URL 32-byte url-safe base64 Fernet key. Pexels keys are encrypted at rest. Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())".
HOST / PORT no Default 127.0.0.1:8000. Docker flips host to 0.0.0.0.
LOG_LEVEL no (INFO) Standard Python levels.
LOG_FORMAT no json (default in HTTP) or text (default in stdio).
PEXELS_API_KEY stdio only Default key for local clients. Ignored in HTTP mode.

Persistent sessions (Redis, optional but recommended in prod)

Without REDIS_URL, OAuth state is in-memory and every Koyeb deploy forces users to re-walk /setup. With Redis, sessions survive restarts. The bound Pexels key is encrypted client-side with Fernet (AES-128-CBC + HMAC-SHA256) before being written — a leaked Redis dump alone yields opaque ciphertext.

Compatible providers: Upstash Redis (free tier 10k cmd/day, 256 MB, TLS), Redis Cloud, self-hosted. See docker-compose.yml for the local dev setup.

Koyeb (one-command deploy)

koyeb service create pexels-mcp \
  --git github.com/VictorNain26/pexels-mcp-server \
  --git-branch main \
  --git-builder docker \
  --ports 8000:http \
  --routes /:8000 \
  --checks 8000:http:/healthz \
  --env TRANSPORT=streamable-http \
  --env "MCP_SERVER_URL=https://{{ KOYEB_PUBLIC_DOMAIN }}" \
  --env "MCP_ALLOWED_HOSTS={{ KOYEB_PUBLIC_DOMAIN }}" \
  --env LOG_FORMAT=json \
  --instance-type nano \
  --regions fra

Then add REDIS_URL + MCP_ENCRYPTION_KEY for persistent sessions.

Smoke test

URL=https://<your-service>.koyeb.app
curl -s "$URL/healthz"   # -> ok
curl -s "$URL/.well-known/oauth-protected-resource" | head -20
curl -i -X POST "$URL/mcp" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json,text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -d '{}' | head -10
# -> 401 with WWW-Authenticate: Bearer ... resource_metadata="..."

Connect a client

Client Steps
claude.ai web Settings → Connectors → Add custom connector → URL https://<host>/mcp. Click Connect. Paste your Pexels key on the /setup page.
Claude Desktop Settings → Connectors → Add (remote) → same URL. Same /setup flow.
Claude Code claude mcp add pexels --transport http https://<host>/mcp.
MCP Inspector npx @modelcontextprotocol/inspector → paste the URL.

Local development

git clone https://github.com/VictorNain26/pexels-mcp-server
cd pexels-mcp-server
uv sync --all-extras

HTTP server (prod parity)

TRANSPORT=streamable-http HOST=127.0.0.1 PORT=8000 \
  MCP_SERVER_URL=http://127.0.0.1:8000 \
  uv run pexels-mcp-server

Full stack with Redis (Fernet path exercised)

echo "MCP_ENCRYPTION_KEY=$(python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')" > .env
docker compose up --build

Stdio (Cursor, local scripts)

PEXELS_API_KEY=your-key uv run pexels-mcp-server

Stdio bypasses OAuth — the key comes from the env var directly.

Check suite

uv run ruff check && uv run ruff format --check
uv run mypy src
uv run python -m pytest

Response shape

pexels_search_photos(query="paris", per_page=1) ships:

  • structuredContent (canonical payload, machine-readable, ~600c):
{
  "page": 1,
  "per_page": 1,
  "count": 1,
  "has_more": true,
  "next_page": 2,
  "total_results": 8000,
  "photos": [
    {
      "id": 28448939,
      "alt": "Vibrant street view of central Paris ...",
      "page_url": "https://www.pexels.com/photo/.../28448939/",
      "photographer": "Sergey Guk",
      "photographer_url": "https://www.pexels.com/@sergeyguk",
      "width": 4000,
      "height": 6000,
      "image_url": "https://images.pexels.com/photos/28448939/.../original.jpeg"
    }
  ]
}
  • content[0] (45-char marker): "See structuredContent for the result payload."

The marker exists so backwards-compat clients reading content see a non-empty block. Modern clients (claude.ai web, Claude Desktop, MCP Inspector 0.10+) consume structuredContent directly.

Three usage examples

1. Hero image with brand color and aspect ratio

pexels_search_photos(
  query="modern open-plan office workspace",
  orientation="landscape",
  size="large",
  color="blue",
  aspect_ratio="16:9",
  min_width=1920,
  per_page=6,
)

2. 4K B-roll, fixed aspect

pexels_search_videos(
  query="aerial drone shot of mountain lake at dawn",
  orientation="landscape",
  size="large",
  aspect_ratio="16:9",
  per_page=10,
)

video_url is the direct MP4 of the top-resolution variant.

3. Drill into a Pexels collection

pexels_get_collection_media(collection_id="9j5dhpu", per_page=20)

The response splits photos[] and videos[]. Filter to one type with type="photos" or type="videos".

Rate limits and attribution

Pexels free tier: 200 requests/hour, 20 000 requests/month on the caller's key (per Pexels' API docs). The server warns to stderr below 100 remaining; the response envelope does not carry rate-limit metadata (saves tokens — flip LOG_LEVEL=DEBUG if you need it).

If you publish anything returned by this server you must credit the photographer / videographer and link back to Pexels per the Pexels licence. Every tool, resource and prompt is shaped so the LLM sees photographer / uploader_name and matching URLs and can surface them in the user-facing answer.

Architecture notes

  • 3-of-3 MCP primitives. Tools (model-controlled), Resources (app-controlled, URI templates per RFC 6570), Prompts (user-controlled, surfaced in claude.ai's connector menu).
  • Spec-compliant auth. OAuth 2.1 Resource Server + Authorization Server in one process via the MCP Python SDK's OAuthAuthorizationServerProvider. RFC 9728 PRM, RFC 8414 ASM, RFC 7591 DCR, PKCE — all served by the SDK. The only custom routes are GET / (landing) and GET/POST /setup (BYOK form).
  • Stateless HTTP by default. stateless_http=True, json_response=True so deployment scales horizontally without sticky sessions. Trade-off: no sampling / no ctx.report_progress / no resource subscriptions — documented in CLAUDE.md.
  • Read-only by construction. Every tool advertises readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=true plus a title.
  • Structured tool output + isError=true. Tools return a TypedDict; the SDK auto-generates outputSchema. Errors raise → FastMCP wraps in CallToolResult(isError=true) per SEP-1303.
  • Strict inputs. Pydantic v2 with extra="forbid"; invalid values come back as Invalid parameters: <field>: <reason>.
  • Token-lean payloads. See the Token economy section above.
  • SDK patches in _sdk_patches.py. The only place in the repo that mutates third-party state.

Health and probes

GET /healthz (liveness) and GET /readyz (readiness) return 200 ok and bypass auth. The Dockerfile declares HEALTHCHECK against /healthz.

Compatibility

  • Python 3.10, 3.11, 3.12.
  • mcp SDK pinned >=1.25,<2.
  • Transport: stdio + Streamable HTTP. Legacy SSE is not enabled.
  • MCP spec 2025-11-25 (SDK negotiates downgrade to 2025-06-18 / 2025-03-26).

See SECURITY.md to report a vulnerability, PRIVACY.md for what the server does and doesn't store.

License

MIT. See LICENSE.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选