Metadata-Version: 2.5
Name: pointguard-mcp
Version: 0.5.0
Summary: MCP server for Creative Market's Pointguard search platform — discover fonts, graphics, and creative assets through natural conversation.
Project-URL: Homepage, https://github.com/creativemarket/pointguard-mcp
Project-URL: Repository, https://github.com/creativemarket/pointguard-mcp
Author: Creative Market
License: MIT
License-File: LICENSE
Keywords: ai,creative-market,fonts,mcp,search
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: mcp<3,>=2
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: http
Requires-Dist: starlette>=0.40; extra == 'http'
Requires-Dist: uvicorn>=0.30; extra == 'http'
Description-Content-Type: text/markdown

# Pointguard MCP Server

An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that exposes Creative Market's Pointguard search platform to AI assistants. Discover fonts, graphics, templates, and creative assets through natural conversation.

## Quick Start

The server is hosted at `https://mcp.creativemarket.com/mcp`. It is public and read-only — no
account, API key or header is needed — so point any MCP client straight at it.

**Claude Code:**

```bash
claude mcp add --transport http pointguard https://mcp.creativemarket.com/mcp
```

**Claude Desktop, Cursor, or claude.ai:** add a custom connector with that URL and no
authentication — see [Usage](#usage) for the click-through steps and JSON.

Then open a chat and try:

> *"Find me a condensed sans-serif for a sports brand logo"*

To run the server yourself instead of using the hosted one, see [Run locally](#run-locally).

---

## Features

| Tool | Description |
|------|-------------|
| `search_products` | Search by style, mood, subject, or keyword with natural language. Filters: category, price, staff pick, on sale, featured, recency, font width and weight, asset properties |
| `get_product` | Internal Pointguard index metadata for agent reasoning — numeric shop ID, category IDs, file type IDs, visibility, staff pick flag |
| `browse_category` | Explore products by classification using the full CM category taxonomy |
| `browse_shop` | Browse a designer or studio's full catalog by shop ID |
| `find_similar` | Find visually/stylistically similar products using embeddings |
| `autocomplete` | Get search suggestions from partial input |
| `suggested_searches` | Discover related searches and trending terms |
| `get_product_details` | Display-ready product data from the CM API — title, URL, full image gallery, description, shop name, formatted price. Accepts up to 50 IDs |
| `identify_font` | Identify the font in an image of text (URL or base64) via Matcherator and return the closest Creative Market font products, closest first |

`identify_font` sends the image, or the image URL for the service to fetch, to Creative Market's Matcherator font-identification service; the other tools send only search parameters.

## Rich results (MCP Apps)

Search results come back three ways at once, so every client gets the best it can render:

- **Markdown** — one block per product with title, starting price, a thumbnail image and an attributed link. Any client that renders markdown shows the pictures, and the model can present the block verbatim.
- **Image blocks** — each thumbnail is also attached as an `ImageContent` block so the model can see what a product looks like. Pass `include_images: false` on a call to skip them.
- **An interactive grid** — hosts that negotiate the [MCP Apps](https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/) extension render `search_products`, `browse_category`, `browse_shop` and `find_similar` results in a scrollable grid of thumbnail, title and price, served from `ui://pointguard-mcp/product-grid`. For those hosts the server attaches the product data as `structuredContent` and skips the image blocks by default, since the grid loads thumbnails itself.

The grid document is `src/pointguard_mcp/ui/product_grid.html` and ships inside the package. It is self-contained (inline CSS and JS), talks to the host only over the MCP Apps `postMessage` protocol, and loads images solely from `tint.creativemarket.com`, which the resource declares in its CSP metadata.

### Testing the grid locally

You do not need Claude Desktop to see the grid. `dev/preview.html` is a small host harness — it embeds the grid, performs the MCP Apps handshake, and delivers a result, exactly as a real host does. It is not part of the package.

```bash
# 1. (optional, for live data) run the server in HTTP mode
MCP_HTTP=1 MCP_PORT=8080 pointguard-mcp &

# 2. serve the repo over http (file:// will not work — CSP and postMessage need an origin)
python3 -m http.server 8000

# 3. open the preview
open http://localhost:8000/dev/preview.html
```

- **Load sample data** renders the grid from canned products with no server at all — the fastest way to iterate on the HTML/CSS.
- **Fetch live** calls `search_products` on the server URL you enter (default `http://localhost:8080/mcp`, plus an `x-api-key` if the server sets one) and renders the real result, thumbnails and all.

## Example Queries

Once connected to an MCP-compatible client (Claude, Cursor, etc.), you can ask:

- *"Find me a condensed sans-serif for a sports brand logo."*
- *"Show me retro display fonts for a vinyl record label."*
- *"I need textures and grunge graphics for a concert poster."*
- *"Find script fonts under $30 that work for wedding stationery."*
- *"Show me font alternatives to Filson Pro Font Family (16 fonts)."* (uses `find_similar`)
- *"What's trending in the Fonts category?"*
- *"Show me everything by Loudoun Design Co."* (uses `browse_shop`)
- *"Browse sans-serif fonts."* (uses `browse_category` with ID 81)

## Installation

```bash
pip install pointguard-mcp
```

Or in development mode:

```bash
cd pointguard-mcp
pip install -e .
```

## Configuration

All environment variables are optional; the server reads:

| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_HTTP` | `0` | `1`/`true` runs an HTTP/SSE server for remote clients; otherwise stdio |
| `MCP_PORT` | `8080` | Port in HTTP mode |
| `MCP_HOST` | `0.0.0.0` | Bind address in HTTP mode |
| `MCP_API_KEY` | _(none)_ | Self-hosted only: if set, HTTP clients must send it as an `x-api-key` header. The hosted server runs without one |
| `POINTGUARD_BASE_URL` | `https://pointguard-public.cm-ops.com` | Pointguard search API base URL |
| `POINTGUARD_TIMEOUT` | `30` | Upstream request timeout, in seconds |
| `CM_PRODUCT_DETAILS_URL` | Creative Market customer API | Endpoint used to enrich results with title, price, and images |
| `MCP_MIN_RESULTS` | `9` | Grid tools raise smaller `size` requests to this floor (set `1` to disable) |
| `MCP_EXCLUDE_AI_GENERATED` | `1` | Exclude assets marked AI-generated from every result (set `0` to include them) |
| `MATCHERATOR_BASE_URL` | `https://www.matcherator.com` | Font-identification service behind `identify_font` |
| `MATCHERATOR_SITE` | `creativemarket_mcp` | Session identifier the service tracks this integration under |
| `MATCHERATOR_TIMEOUT` | `60` | Per-request timeout for the font-identification service, in seconds |
| `OPENAI_APPS_CHALLENGE_TOKEN` | _(none)_ | Token from OpenAI's plugin dashboard, served as plain text at `/.well-known/openai-apps-challenge` for ChatGPT directory domain verification; unset answers 404 |
| `MCP_PUBLIC_URL` | _(none)_ | The server's public origin, e.g. `https://mcp.creativemarket.com`. When set, the MCP handshake advertises `/icon.png` and `/icon.svg` on it; when unset the icons are sent inline as data URIs |

No Creative Market credentials are required — the details endpoint is unauthenticated.

All tools are read-only, and assets marked AI-generated are excluded from results by default.

## Usage

Two ways to use the server:

- **Connect to the hosted server** at `https://mcp.creativemarket.com/mcp` (public, no key) — what most clients want.
- **Run it yourself** for development.

### Run locally

Requires Python 3.10+ (the server is built on `mcp>=2`).

```bash
pip install -e ".[http]"          # or: pip install pointguard-mcp

# stdio — for a client that launches the server as a subprocess
python -m pointguard_mcp.server

# HTTP/SSE on http://localhost:8080 — for remote-style clients; hit /health to check
MCP_HTTP=1 MCP_PORT=8080 python -m pointguard_mcp.server
# add MCP_API_KEY=... to require an x-api-key header
```

Note: the server enriches results against Creative Market, which sits behind a WAF
that allowlists the production egress IP, so live search/enrichment may not work from
an arbitrary machine. To preview the grid UI with sample data and no network, see
[Testing the grid locally](#testing-the-grid-locally).

### With Claude Code

Hosted server:

```bash
claude mcp add --transport http pointguard https://mcp.creativemarket.com/mcp
```

A local server you started with `MCP_HTTP=1` (add `--header "x-api-key: ..."` only if you set `MCP_API_KEY`):

```bash
claude mcp add --transport http pointguard-local http://localhost:8080/mcp
```

Add `--scope user` to make it available in every project. Manage with `claude mcp list`
and `claude mcp remove pointguard`. Then ask, e.g., *"search Creative Market for vintage
script fonts."*

> Claude Code is a terminal client and does not render the MCP Apps grid — it shows the
> tools and their text/markdown output. For the visual grid, use Claude Desktop or claude.ai.

### With Claude Desktop or Cursor

Connect to the hosted server. Recent builds can add it from the UI — **Settings → Connectors
→ Add custom connector**, paste `https://mcp.creativemarket.com/mcp`, and leave
authentication off — or use the JSON in [Remote Client Setup Example](#remote-client-setup-example-mcpcreativemarketcommcp)
below (native `type: http`, or the `mcp-remote` bridge).

To run a local server as a subprocess instead, point the client at the `pointguard-mcp`
command (stdio) with `POINTGUARD_BASE_URL` set to a reachable Pointguard API.

## Agent Guide (Full Project Map)

This section is a practical guide for coding agents and maintainers working on the full MCP project.

### Repository Layout

- `src/pointguard_mcp/server.py`: MCP server entrypoint, tool schemas, tool dispatch, HTTP/SSE transport routes.
- `src/pointguard_mcp/client.py`: Async HTTP client for Pointguard API and Creative Market detail API.
- `pyproject.toml`: Package metadata and CLI entrypoint (`pointguard-mcp = pointguard_mcp.server:main`).
- `terraform/`: AWS infra for ECR + ECS Fargate + ALB deployment.
- `Dockerfile`: Container image used for remote deployment.

### Runtime Modes

- Default mode is stdio (for Claude Desktop, Cursor command-based config, and local agent use).
- Set `MCP_HTTP=1` to run as an HTTP server with:
  - `GET/POST/DELETE /mcp` for Streamable HTTP transport
  - `GET /sse` + `POST /messages/` for legacy SSE transport
  - `GET /icon.png` and `GET /icon.svg` — the Creative Market mark advertised in the MCP handshake; `src/pointguard_mcp/assets/icon.png` is also the file to upload as the directory listing's icon

### Transport and Auth Behavior

- The hosted server runs unauthenticated. The Connectors Directory requires OAuth 2.0 for
  authenticated connectors, and the catalog is public, read-only data, so no key is set in
  production. Abuse protection lives at the Cloudflare edge: `mcp.creativemarket.com` is
  proxied, the ALB accepts traffic only from Cloudflare's IP ranges (`terraform/ecs.tf`), and
  rate limiting is a Cloudflare rule.
- Self-hosted deployments can set `MCP_API_KEY`; HTTP calls to `/mcp` and `/sse` must then
  include header `x-api-key`.
- In stdio mode, no HTTP auth is involved because transport is local process pipes.
- For remote clients:
  - Streamable HTTP clients should use `/mcp`.
  - SSE clients should use `/sse`.

### Tooling Contract

- Search and discovery tools (`search_products`, `browse_category`, `browse_shop`, `find_similar`) return Pointguard index-oriented results.
- `get_product` returns internal metadata useful for agent planning (shop/category/file-type IDs, flags).
- `get_product_details` returns display-ready Creative Market data (title, URL, description, images, shop).

### Typical Agent Flow

1. Run `search_products` with natural language query.
2. Use `get_product` when internal metadata is needed for reasoning/filtering.
3. Use `browse_shop` or `browse_category` for expansion.
4. Use `get_product_details` to produce user-facing final answers.

### Deployment Notes for Agents

- ECS/ALB deployment lives in `terraform/`.
- ALB TLS now depends on `acm_certificate_arn`:
  - empty value: HTTP-only listener on port 80
  - non-empty value: HTTPS listener on 443 + HTTP 301 redirect to HTTPS
- Cloudflare setup for custom domain usually needs:
  - ACM validation CNAME (DNS only)
  - service CNAME `mcp` -> ALB DNS name (typically proxied)

### Local Validation Checklist

```bash
# package and imports
pip install -e .

# stdio server starts
python -m pointguard_mcp.server

# HTTP server starts
MCP_HTTP=1 MCP_PORT=8080 python -m pointguard_mcp.server

# health check
curl -i http://localhost:8080/health
```

### Common Failure Modes

- `301 Moved Permanently` from internal API host: use HTTPS base URL or allow redirects.
- TLS hostname mismatch on internal ELB: certificate does not match host; use a valid host/cert path for production.
- Remote client `Invalid URL`: missing scheme (must be `https://...`, not bare host).
- Remote client `ECONNREFUSED` on `https://...`: no HTTPS listener configured on origin ALB or DNS/proxy mismatch.

### Deployment Runbook (GitHub Actions + Terraform)

Use this runbook when shipping code or infrastructure changes for the MCP service.

#### A) Application Deploy via GitHub Actions

This repo includes workflows under `.github/workflows/`:

- `build.yml`: builds and pushes Docker images to ECR (on `main` pushes, releases, or manual dispatch)
- `publish.yml`: package publishing flow

Recommended flow for runtime changes:

1. Merge code changes to `main`.
2. Confirm `Build and Push Container` succeeds in GitHub Actions.
3. Record the image tag from workflow output (`sha-xxxxxxx` on push, release tag on release event).
4. Trigger infrastructure rollout using Terraform (next section).

Required GitHub environment secrets (as used by current workflows):

- `ECS_ADMIN_DEPLOY_KEY` (for `build.yml`)
- `ECS_ADMIN_DEPLOY_SECRET` (for `build.yml`)

Recommended additional CI checks before merge:

1. Run `python -m pointguard_mcp.server` (stdio smoke test).
2. Run `MCP_HTTP=1 MCP_PORT=8080 python -m pointguard_mcp.server` and verify `/health`.
3. Confirm no tool schema regressions in `server.py`.

#### B) Infrastructure Deploy via Terraform (ECS/ALB path)

Terraform config is in `terraform/` and is intended for ECS + ALB deployments.

Recommended Terraform workflow:

1. Update Terraform code in `terraform/`.
2. Run plan in Terraform Cloud (or local CLI) and review listener/DNS/cert diffs carefully.
3. Apply in the target workspace.
4. Validate outputs and ALB listeners.

Important variables for HTTPS custom domain setup:

- `acm_certificate_arn`: when set, enables ALB HTTPS listener `:443` and HTTP `:80` redirect to HTTPS
- `pointguard_base_url`
- `cm_product_details_url`
- `image_tag`

Post-apply validation checklist:

1. ALB listener `443/HTTPS` exists and uses expected ACM cert.
2. ALB listener `80/HTTP` redirects to `443`.
3. Cloudflare DNS includes:
  - ACM validation CNAME (DNS only)
  - `mcp` CNAME to ALB (proxied, unless your network policy requires DNS-only)
4. Health endpoint is reachable:

```bash
curl -i https://mcp.creativemarket.com/health
```

#### C) End-to-end Release Order (safe sequence)

1. Merge and let `build.yml` push a fresh image to ECR.
2. Apply Terraform changes (or run ECS force-new-deployment if only code changed and task definition already references latest image).
3. Verify ALB listeners and health endpoint.
4. Verify MCP transport in Inspector, then in client apps.

### PyPI Release Process

This repo uses GitHub Trusted Publishing for PyPI via `.github/workflows/publish.yml`.
Publishing is triggered by creating a GitHub Release with status `published`.

#### 1) Bump package version

Update `version` in `pyproject.toml` (must be higher than current PyPI version).

#### 2) Merge to main

Open/merge a PR with the version bump and release changes.

#### 3) Create GitHub release

In GitHub UI:

1. Go to Releases -> Draft a new release.
2. Create tag `vX.Y.Z` matching the version in `pyproject.toml`.
3. Publish the release.

This triggers `publish.yml` which:

1. Runs smoke tests.
2. Builds wheel/sdist.
3. Publishes to PyPI with OIDC trusted publishing.

#### 4) Verify publish

```bash
python -m pip index versions pointguard-mcp
```

You should see the new version listed as latest.

#### Notes

- Re-publishing the same version will fail on PyPI.
- If publish fails, check GitHub Action logs for `publish.yml` first (test stage or trusted publishing permissions).

### MCP Inspector UI Debugging Guide

Use MCP Inspector to isolate transport/auth issues before testing in Claude or Cursor.

#### 1) Open Inspector

```bash
npx @modelcontextprotocol/inspector
```

#### 2) Test Streamable HTTP endpoint

- Transport: `Streamable HTTP`
- URL: `https://mcp.creativemarket.com/mcp`
- Authentication: none

Expected result:

- Connect succeeds
- Tools list loads
- You can call `search_products`

#### 3) If Streamable HTTP fails, test SSE endpoint

- Transport: `SSE`
- URL: `https://mcp.creativemarket.com/sse`
- Authentication: none

#### 4) Error-to-cause quick map

- `Invalid URL`: URL missing scheme (`https://`)
- `ECONNREFUSED`: origin not accepting requested protocol/port (often no ALB HTTPS listener)
- `401 Unauthorized`: a self-hosted server with `MCP_API_KEY` set did not get a matching `x-api-key` (the hosted server never returns this)
- `301 redirect loops/unexpected redirects`: mismatch between client endpoint and listener redirect policy

### Operational Notes

- The hosted server has no `MCP_API_KEY`. If you set one on a self-hosted deployment, keep it in a secret store; do not hardcode it in repo files.
- For internal Pointguard APIs, prefer `https://` base URLs to avoid redirect overhead.
- If using internal ELB hostnames with cert mismatch, use proper cert/hostname pairing for production-grade security.
- When troubleshooting client startup, inspect local logs first (for Claude Desktop: `~/Library/Logs/Claude/mcp.log`).

## Category Reference

### Top-level Categories

| ID | Category |
|----|----------|
| 1 | Graphics |
| 2 | Templates |
| 3 | Fonts |
| 4 | Add-ons (Brushes & More) |
| 6 | Photos |
| 7 | 3D |
| 8 | Stock Video / Footage |
| 23 | Illustrations |
| 26 | Icons |
| 1800 | Mockups |

### Font Subcategories

| ID | Subcategory |
|----|-------------|
| 80 | Serif |
| 81 | Sans Serif |
| 82 | Script |
| 84 | Symbols |
| 85 | Blackletter |
| 86 | Slab Serif |
| 88 | Handwritten |

### Template Subcategories

| ID | Subcategory |
|----|-------------|
| 50 | Logos |
| 19 | Posters |
| 42 | Flyers |
| 46 | Invitations |
| 45 | Business Cards |
| 49 | Presentations |
| 52 | Social Media |
| 44 | Stationery |
| 41 | Brochures |
| 43 | Resumes |

### Other Notable Subcategories

| ID | Subcategory |
|----|-------------|
| 27 | Textures (Graphics) |
| 28 | Patterns (Graphics) |
| 24 | Objects (Graphics) |
| 131 | Nature (Photos) |
| 132 | People (Photos) |
| 127 | Food & Drink (Photos) |
| 21 | Product Mockups |
| 1801 | Device Mockups |
| 1805 | Apparel Mockups |
| 2601 | Solid Icons |
| 2602 | Outline Icons |
| 2603 | Duotone Icons |

See `CATEGORY_MAP` in `server.py` for the full list of 170+ category and subcategory mappings.

## Development

```bash
# Install in development mode
pip install -e .

# Run directly (stdio)
python -m pointguard_mcp.server

# Run as HTTP/SSE server locally
MCP_HTTP=1 MCP_API_KEY=secret python -m pointguard_mcp.server

# Test with MCP inspector (browser UI)
npx @modelcontextprotocol/inspector pointguard-mcp
```

## Deploying to AWS (HTTP/SSE mode)

The server supports HTTP/SSE transport for remote deployment. Set `MCP_HTTP=1` to switch from stdio to HTTP.

### Environment Variables (HTTP mode)

| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_HTTP` | `0` | Set to `1` to enable HTTP/SSE transport |
| `MCP_HOST` | `0.0.0.0` | Host to bind to |
| `MCP_PORT` | `8080` | Port to listen on |
| `MCP_API_KEY` | _(optional)_ | Self-hosted only: if set, HTTP clients must send `x-api-key: <value>`. Unset in production |

### ECS + ALB Deployment (production)

This repository deploys to AWS ECS Fargate behind an ALB using the Terraform in `terraform/`.

#### 1) Build and publish image to ECR

Use GitHub Actions `build.yml` (recommended), or build manually:

```bash
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
docker build -t pointguard-mcp .
docker tag pointguard-mcp:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/pointguard-mcp:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/pointguard-mcp:latest
```

#### 2) Apply Terraform infrastructure and service configuration

Ensure workspace variables are set (or defaults are correct):

- `acm_certificate_arn` (for HTTPS listener on 443)
- `pointguard_base_url`
- `cm_product_details_url`
- `image_tag`

Then run Terraform plan/apply in `terraform/`.

#### 3) Force ECS service rollout when needed

If only image tag/state changed and you need immediate recycle:

```bash
aws ecs update-service \
  --region us-east-1 \
  --cluster pointguard-mcp \
  --service pointguard-mcp \
  --force-new-deployment
```

Optional wait:

```bash
aws ecs wait services-stable \
  --region us-east-1 \
  --cluster pointguard-mcp \
  --services pointguard-mcp
```

#### 4) DNS and TLS

For `mcp.creativemarket.com`:

1. Keep ACM validation CNAME record as DNS-only in Cloudflare.
2. Point `mcp` CNAME to the ALB DNS name.
3. Keep the `mcp` record proxied. The ALB accepts traffic only from Cloudflare's IP ranges, so a DNS-only record would not reach the origin.

#### 5) Smoke tests

```bash
curl -i https://mcp.creativemarket.com/health
curl -i -X POST https://mcp.creativemarket.com/mcp \
  -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
# expect HTTP 200 with serverInfo and an mcp-session-id header; a 401 means a key is set where it should not be
```

### Remote Client Setup Example (mcp.creativemarket.com/mcp)

For clients with native remote Streamable HTTP support:

```json
{
  "mcpServers": {
    "pointguard-public": {
      "type": "http",
      "url": "https://mcp.creativemarket.com/mcp"
    }
  }
}
```

For Claude Desktop, if direct remote transport is skipped in your build, use an `mcp-remote` bridge:

```json
{
  "mcpServers": {
    "pointguard-public": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.creativemarket.com/mcp",
        "--transport",
        "http-only"
      ]
    }
  }
}
```

## ChatGPT plugin package

The ChatGPT directory lists the same hosted server as a plugin: an [Agent Plugins](https://agent-plugins.org)
package under [`chatgpt-plugin/`](chatgpt-plugin/) with `plugin.json` (listing copy, review test cases,
release notes), `mcp.json` (the server, no authentication), the icon, and one skill,
`creative-market-art-direction`, which guides the model to assemble cross-category kits.

- **Build the ZIP** with the reviewer-accessible demo recording URL, which is review material kept
  out of the repo: `python scripts/build_chatgpt_plugin.py --demo-url https://...`. The script
  validates OpenAI's limits (five positive and three negative test cases, copy lengths, URLs, no
  credentials) and writes `dist/creative-market-plugin-<version>.zip`. `--check` validates only;
  the test suite runs it.
- **Domain verification.** OpenAI's dashboard issues a token in its "Connect MCP server" step and
  expects it back as plain text from `https://mcp.creativemarket.com/.well-known/openai-apps-challenge`.
  Set it as `OPENAI_APPS_CHALLENGE_TOKEN` (Terraform `openai_apps_challenge_token`) and apply; the
  route answers 404 until then.
- **After approval**, server changes are picked up by OpenAI's daily scans with no new ZIP. Changes
  to `plugin.json` or the skill need a new package upload, which may re-trigger review.

## MCP Registry listing

The hosted server is listed in the official [MCP Registry](https://registry.modelcontextprotocol.io) as
`com.creativemarket/creative-market`, titled "Creative Market", from [`server.json`](server.json).
Aggregators such as PulseMCP, Smithery and Glama ingest the registry, so the listing is maintained in
one place. It is remote-only: it points at `https://mcp.creativemarket.com/mcp` and ships no package,
because a locally run server cannot reach the WAF-protected enrichment API.

- **Copy.** The registry caps `description` at 100 characters and is client-agnostic, so the manifest
  carries the approved tagline rather than the Claude directory's long description. Edit copy in
  `server.json`; `tests/test_registry_manifest.py` enforces the limits.
- **Versions.** Every publish needs a new version, and the registry version is the package version.
  [`registry.yml`](.github/workflows/registry.yml) runs when `pyproject.toml` or `server.json` changes
  on `main`: it writes the package version into `server.json`, validates it against the official
  schema, skips versions already in the registry, and publishes. Bump the version in `pyproject.toml`
  to republish; a metadata-only republish can use a prerelease such as `0.5.0-1`.
- **Namespace proof.** `com.creativemarket` is proven by DNS: a TXT record on the apex of
  `creativemarket.com` of the form `v=MCPv1; k=ed25519; p=<public key>`, with the matching Ed25519
  private key held as the `MCP_PRIVATE_KEY` secret of the `mcp-registry` GitHub environment. The key
  pair lives in 1Password. Rotating it means updating both, and removing the old TXT record, which the
  registry tries first.
- **Publishing from a laptop** instead of CI: `brew install mcp-publisher`, then
  `mcp-publisher login dns --domain creativemarket.com --private-key <hex>` and `mcp-publisher publish`
  from the repo root.

## Architecture

```
┌─────────────────┐     stdio      ┌──────────────────┐     HTTP/JSON     ┌──────────────────────┐
│  AI Assistant   │◄──────────────►│  pointguard-mcp  │◄────────────────►│  Pointguard API      │
│  (Claude, etc.) │                │  (MCP Server)    │                   │  (Flask + ES)        │
└─────────────────┘                └──────────────────┘                   └──────────────────────┘
                                           │
                                           │ HTTPS
                                           ▼
                                   ┌──────────────────┐
                                   │  creativemarket  │
                                   │  .com (search +  │
                                   │  product detail) │
                                   └──────────────────┘
```

The MCP server has two data sources:

1. **Pointguard API** (`POINTGUARD_BASE_URL`) — powers `search_products`, `get_product`, `browse_category`, `browse_shop`, `find_similar`, `autocomplete`, `suggested_searches`
2. **creativemarket.com API** — powers `get_product_details` (full image gallery, descriptions, licenses via authenticated API)

## Endpoints Used

| Tool | Endpoint | Returns |
|------|----------|---------|
| `search_products` | `POST /creative-market/products/search` | Product IDs + relevance tokens |
| `get_product` | `GET /creative-market/product/{id}` | Internal metadata — shop ID, category IDs, file types, flags |
| `browse_category` | `POST /creative-market/products/category` | Product IDs |
| `browse_shop` | `POST /creative-market/products/shop` | Product IDs |
| `find_similar` | `POST /creative-market/product/{id}/similar` | Product IDs |
| `autocomplete` | `GET /creative-market/completions/suggest` | Suggestion strings |
| `suggested_searches` | `POST /creative-market/suggested-searches` | Related search terms |
| `get_product_details` | `GET https://creativemarket.com/api/external/newsletter/product-details/{ids}` | Display-ready detail (title, URL, images, description, shop URL) |
