{
  "markdown": "# Commonlands MCP\n\nPublic MCP server for Commonlands precision optics. Use it to choose M12/C-mount lenses, calculate field of view, verify live Shopify product truth, and hand buyers to Shopify-owned carts safely.\n\n## Connect\n\n- MCP endpoint: `https://mcp.commonlands.com/mcp`\n- UCP discovery: `https://mcp.commonlands.com/.well-known/ucp`\n- Health check: `https://mcp.commonlands.com/healthz`\n- Client setup details: [`docs/client-connections.md`](docs/client-connections.md)\n- Agent instruction guide: [`docs/agent-instructions.md`](docs/agent-instructions.md)\n- Full live guide: [`docs/live-usage-and-integrations.md`](docs/live-usage-and-integrations.md)\n\n## Copy-paste agent instruction\n\n```text\nUse Commonlands MCP at https://mcp.commonlands.com/mcp for lens selection. Start with tools/list. Catalog EFL, image circle, max FoV/FOV@image-circle, and distortion display fields are insufficient to compute FoV on a specific sensor; do not interpolate or estimate sensor FoV from those fields. Use calculate_field_of_view for one lens/sensor pair, match_lens_to_sensor for sensor/target-FoV selection, search_lens_catalog for broad catalog discovery, and get_lens_distortion_profile for distortion status. Preserve the returned method, distortion_model, coverage_ok, image_circle_mm, sensor_diagonal_mm, and rectilinear_comparison. Call read_shopify_products before stating live price, availability, Product/Variant GIDs, URL, SKU, media, metafields, inventory, or cart payload. Only call create_cart/update_cart after the buyer confirms exact live Variant GIDs and quantities. Checkout tools are not live unless they appear in tools/list. Never ask for card data or perform Shopify catalog/inventory/order/customer writes.\n```\n\n## Agent workflow\n\n1. Call `tools/list` and trust the live list over docs.\n2. For sensor-specific lens finding, call `match_lens_to_sensor` first, then call `calculate_field_of_view` for final candidate lens/sensor FoV claims.\n3. Use `search_lens_catalog` only for broad SKU/title/mount/lens-type discovery. It does not replace per-sensor FoV calculation.\n4. Get per-object grounding with `resources/read` for `commonlands://sensors/{part}` or `commonlands://lenses/{sku}` when needed.\n5. Use `get_lens_distortion_profile` for distortion/model/status questions. Do not invent polynomial coefficients or claim measured correction when the response says source-display-only.\n6. Use `prompts/list` / `prompts/get` with `select_lens_for_sensor_fov_working_distance` when a client surfaces MCP prompts.\n7. Verify purchasable truth with `read_shopify_products` before quoting final SKU, URL, price, availability, Shopify IDs, or cart payloads.\n8. Create/update a Shopify cart only after explicit buyer confirmation of line items and quantities.\n9. Send the buyer to Shopify's returned cart/checkout URL. Do not claim Checkout MCP is live until checkout tools appear in `tools/list`.\n\n## FoV rule\n\nCatalog EFL, image circle, max FoV/FOV@image-circle, and distortion display fields are insufficient to compute field of view on a specific sensor. Agents must not interpolate interior-sensor FoV or substitute their own calculations. Use `calculate_field_of_view`, then preserve returned `hfov_deg`, `vfov_deg`, `dfov_deg`, `method`, `distortion_model`, `coverage_ok`, `image_circle_mm`, `sensor_diagonal_mm`, `rectilinear_comparison`, and provenance/source metadata in the answer.\n\n## Truth hierarchy\n\n1. `read_shopify_products` = live Shopify product truth.\n2. `calculate_field_of_view` / `match_lens_to_sensor` = **live FoV backend** (AWS Lambda + DynamoDB lens catalog) when configured. Sensor inputs resolve through the live DynamoDB sensor catalog with fixture fallback. These are the routed public optics tools.\n3. Compatibility aliases (`compute_fov`, `compute_fov_catalog`, `match_lenses_to_sensor`) still dispatch where practical, but new clients should route through the intent-named tools above.\n4. Ranking tools (`match_lens_to_sensor`, `recommend_lenses_for_application`, `compare_lenses`) rank against **live FoV-backend specs and field of view** when the live backend is enabled, so they use real per-SKU specs (EFL, mount, image circle, FoV). They still exclude live Shopify stock/price/variant IDs; use `read_shopify_products` for purchasable truth. If the live backend is unconfigured they fall back to fixture scaffold.\n5. The remaining fixture catalog/product-page tools = useful engineering context, not final commerce truth. If the live backend is ever unconfigured, FoV tools fail closed and sensor lookups fall back to a small reference fixture.\n\nIf fixture data conflicts with `read_shopify_products` or the live FoV/sensor backends, use the live truth.\n\n### Data sources\n\n- **Sensors** (`commonlands://sensors/{part}` and the sensor used by `calculate_field_of_view` / `match_lens_to_sensor`): read from the Commonlands DynamoDB sensor table by part number when configured, with fixture fallback. Pixel pitch and pixel counts come straight from that table; active-area mm is derived as `pixels x pitch`.\n- **Lenses** (`commonlands://lenses/{sku}`, `calculate_field_of_view`, `match_lens_to_sensor`, `search_lens_catalog`): the FoV Lambda reads lens optical parameters from its DynamoDB lens table when configured. Catalog-wide matching covers the full lens table when backend scanning is enabled.\n- **Distortion coefficients** are computed server-side inside the Lambda and are never returned to clients. If the live backend only returns a display distortion string, MCP returns an honest `distortion_model` / `distortion_status` and does not claim measured polynomial correction.\n\n## Current live surface\n\nThe production surface currently exposes **20 tools** across catalog/search, FoV, Shopify read-only, cart, UCP catalog, and purchase-handoff. Checkout tools, `cancel_cart`, and `read_shopify_metaobjects` are not exposed. Always trust the live `tools/list` over any doc.\n\nKey tools:\n\n- Public optics routing: `calculate_field_of_view`, `match_lens_to_sensor`, `search_lens_catalog`, `get_lens_distortion_profile`.\n- Catalog/context (in `tools/list`): `search_catalog`, `lookup_catalog`, `get_product`, `get_product_page_details`, `compare_lenses`, `recommend_lenses_for_application`.\n- Legacy hidden aliases (still dispatch for old clients, not listed in `tools/list`): `compute_fov`, `compute_fov_catalog`, `match_lenses_to_sensor`, `search_lenses`, `get_lens_details`.\n- Resources/prompts: `commonlands://sensors/{part}`, `commonlands://lenses/{sku}`, `commonlands://catalog/sensors`, `commonlands://catalog/lenses`, and prompt `select_lens_for_sensor_fov_working_distance`.\n- Live Shopify read-only truth: `read_shopify_products`, `get_shopify_readonly_config_status`.\n- Buyer-confirmed Shopify cart handoff: `create_cart`, `get_cart`, `update_cart` when visible in `tools/list`.\n- RFQ / question handoff: `submit_rfq` forwards a buyer quote request or question to the fixed Commonlands engineering inbox (SendGrid). The agent cannot choose the recipient; it sends an inquiry only (no order, payment, or Shopify write) and stays inert (routes to the contact page) until `SENDGRID_API_KEY` + `RFQ_TO_EMAIL` + `RFQ_FROM_EMAIL` are configured.\n- Diagnostics/readiness: `get_catalog_snapshot_status`, `get_shopify_ucp_readiness`, `prepare_shopify_purchase_handoff`, `get_purchase_route_options`.\n\n## Public-data scope (Shopify reads)\n\nThe endpoint is public and unauthenticated, so `read_shopify_products` returns\n**only data that is already public on commonlands.com** (enforced server-side\nin `src/shopify-read-adapter.ts`, see `PUBLIC_DATA_POLICY`):\n\n- **Active products only.** DRAFT and ARCHIVED products are filtered out and\n  the internal `status` field is never returned.\n- **No exact inventory.** Variants carry a coarse `availability` signal\n  (`in_stock` / `low_stock` / `out_of_stock` / `untracked`); raw counts and\n  inventory item IDs are never returned.\n- **Metafields are opt-in and allowlisted.** `includeMetafields` defaults to\n  `false`; when enabled, only the `custom.*` display fields rendered on public\n  product pages are returned. App/private namespaces and non-allowlisted keys\n  (including `custom.docsend_page`) are always dropped.\n- **`read_shopify_metaobjects` was removed** from the public surface\n  (2026-07): Admin metaobject definitions can hold non-public store content.\n  Calls return an actionable error pointing to `read_shopify_products`.\n- The requested Admin scopes were narrowed to the read scopes the surface\n  actually needs (no metaobject, marketing, payment-terms, or shipping\n  scopes).\n\n## Abuse controls (cart and endpoint)\n\nCart tools are deliberately reachable without authentication — the same trust\nmodel as Shopify's own public Storefront cart — but they are bounded:\n\n- **Per-IP rate limits** (Workers Rate Limiting API, `wrangler.toml`):\n  120 requests/min per IP across `/mcp`, and 10 cart mutations/min per IP for\n  `create_cart`/`update_cart`. Exceeding either returns HTTP 429 with\n  `retry-after: 60`.\n- **Strict payloads:** 1–25 line items per cart, quantity 1–999 per line, and\n  item IDs must be live Shopify ProductVariant GIDs — fixture IDs, SKUs, and\n  numeric IDs are rejected before any Shopify call.\n- **Carts are Shopify-owned and expire.** The Worker is a stateless proxy;\n  cart state, TTL, and expiry (`expiryAuthority: shopify_cart_ttl_expires_at`)\n  belong to Shopify, which prunes abandoned carts automatically. No cart,\n  session, customer, or payment state is ever stored in the Worker.\n- **No checkout surface:** checkout/cancel tools are hidden; the server cannot\n  take payment, create orders or customers, apply discounts, or write\n  inventory, so an abusive caller cannot mutate anything durable.\n\n## Safety boundaries\n\n- Do not use fixture prices, availability, product URLs, SKU variants, or IDs as final commerce truth.\n- Do not create or update carts unless the buyer has confirmed live Variant GIDs and quantities.\n- Do not use Checkout MCP yet; checkout endpoints will be enabled later after validation and approval.\n- Do not ask for or transmit card numbers, CVV/CVC, payment tokens, passwords, or customer account credentials.\n- Do not perform Shopify product, variant, collection, tag, metafield, inventory, order, customer, discount, RFQ, Acumatica, or database writes.\n- Do not expose gated datasheet URLs or backend secrets.\n- For live FoV, call Commonlands MCP only. Do not call the AWS Lambda/API Gateway backend directly.\n\n## Good prompts\n\n- `Find M12 lenses for IMX477 around 50° horizontal FoV. Use match_lens_to_sensor, calculate_field_of_view, then verify the final purchasable SKU with read_shopify_products.`\n- `Compare CIL078 and CIL250 on IMX477. Preserve rectilinear_comparison and label fixture-backed context separately from live Shopify truth.`\n- `Find the live Shopify Product and Variant GID for CIL250. Return URL, SKU, price, inventory signal, and cart path, but do not create a cart.`\n- `Create a Shopify cart for two units of this live Variant GID: <gid>. The buyer has confirmed quantity 2.`\n- `List Commonlands MCP tools and classify each as fixture context, live FoV, live Shopify read-only, or Shopify cart.`\n\n## Observability\n\nThe Worker can write privacy-safe request/tool telemetry when a Cloudflare Analytics Engine binding named `MCP_ANALYTICS` is configured. Telemetry records only method, path, MCP method, tool name, status, client label, environment/version, HTTP status, and duration. It does **not** record request arguments, Shopify payloads, customer data, product IDs, cart IDs, secrets, or response bodies.\n\nExample binding:\n\n```toml\n[[analytics_engine_datasets]]\nbinding = \"MCP_ANALYTICS\"\ndataset = \"commonlands_mcp_events\"\n```\n\nAfter deploy, verify that telemetry is live:\n\n```bash\ncurl https://mcp.commonlands.com/healthz\ncurl -X POST https://mcp.commonlands.com/mcp \\\n  -H 'content-type: application/json' \\\n  -H 'accept: application/json, text/event-stream' \\\n  -H 'mcp-client-name: telemetry-smoke' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"telemetry-smoke\",\"method\":\"tools/list\",\"params\":{}}'\n```\n\n`/healthz` should report `\"telemetry\":{\"analyticsEngine\":\"configured\"}`. Then query the `commonlands_mcp_events` Analytics Engine dataset. Column order is `blob1=request method`, `blob2=path`, `blob3=MCP method`, `blob4=tool`, `blob5=status`, `blob6=client`, `blob7=environment`, `blob8=version`, `double1=HTTP status`, and `double2=duration ms`.\n\nTool usage rollup:\n\n```bash\ncurl \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/analytics_engine/sql\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_ANALYTICS_READ_TOKEN\" \\\n  --data \"SELECT blob4 AS tool, blob5 AS status, SUM(_sample_interval) AS calls, SUM(_sample_interval * double2) / SUM(_sample_interval) AS avg_duration_ms FROM commonlands_mcp_events WHERE timestamp >= NOW() - INTERVAL '7' DAY AND blob3 = 'tools/call' GROUP BY tool, status ORDER BY calls DESC LIMIT 50 FORMAT JSON\"\n```\n\nClient/tool rollup:\n\n```bash\ncurl \"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/analytics_engine/sql\" \\\n  -H \"Authorization: Bearer $CLOUDFLARE_ANALYTICS_READ_TOKEN\" \\\n  --data \"SELECT blob6 AS client, blob4 AS tool, SUM(_sample_interval) AS calls, SUM(_sample_interval * double2) / SUM(_sample_interval) AS avg_duration_ms FROM commonlands_mcp_events WHERE timestamp >= NOW() - INTERVAL '7' DAY AND blob3 = 'tools/call' AND blob5 = 'ok' GROUP BY client, tool ORDER BY calls DESC LIMIT 100 FORMAT JSON\"\n```\n\nUse `blob4` to see which MCP tools agents actually call. High-call/high-success tools are candidates for deeper investment; low-call or repeated-error tools are candidates for better descriptions, consolidation, or deprecation. Keep Cloudflare invocation logs enabled for request/response metadata, but use Analytics Engine for tool-level decisions because it captures the JSON-RPC method and tool name without storing request arguments.\n\n## Quick client setup\n\n### Codex\n\n```toml\n[mcp_servers.commonlands]\nurl = \"https://mcp.commonlands.com/mcp\"\ntool_timeout_sec = 60\n```\n\n### Claude Desktop / Claude Code via `mcp-remote`\n\n```json\n{\n  \"mcpServers\": {\n    \"commonlands\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"mcp-remote\", \"https://mcp.commonlands.com/mcp\"]\n    }\n  }\n}\n```\n\n### Cursor\n\n```json\n{\n  \"mcpServers\": {\n    \"commonlands\": {\n      \"url\": \"https://mcp.commonlands.com/mcp\"\n    }\n  }\n}\n```\n\n## Configuration\n\nNon-secret config lives in `wrangler.toml` `[vars]`; credentials are Worker secrets set via the Cloudflare dashboard or `wrangler secret put` (never committed).\n\n| Setting | Where | Purpose |\n| --- | --- | --- |\n| `account_id` | `wrangler.toml` | Pins the Cloudflare account so deploys do not call `/memberships` (which an account-scoped API token cannot access, surfacing as auth error `9106`). |\n| `FOV_LIVE_BACKEND_ENABLED` | `[vars]` | `\"true\"` routes FoV through the live Lambda backend. |\n| `FOV_LAMBDA_ENDPOINT` | `[vars]` | Allowlisted FoV Lambda/API Gateway URL. |\n| `FOV_BACKEND_SCANS_FULL_CATALOG` | `[vars]` | `\"true\"` makes `compute_fov_catalog` omit `partNums` so the Lambda scans its full DynamoDB lens table. Requires `ALLOW_LENS_SCAN=true` on the Lambda. When `\"false\"`, the Worker sends fixture SKUs as a fallback. |\n| `SENSOR_DDB_TABLE` | `[vars]` | DynamoDB sensor table name. |\n| `SENSOR_DDB_REGION` | `[vars]` | DynamoDB sensor table region. |\n| `FOV_API_KEY` | **secret** | Shared key the Worker sends to the FoV Lambda (`x-api-key`); must match the Lambda's `FOV_API_KEY` exactly (byte-for-byte, no trailing newline). |\n| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | **secret** | Read-only IAM user credentials the Worker uses to read the sensor DynamoDB table (SigV4). |\n| `CLOUDFLARE_API_TOKEN` | **GitHub Actions secret** | Token with `Workers Scripts: Edit` used by the Deploy workflow. |\n\n### AWS / DynamoDB notes\n\n- The Worker reads the **sensor** table directly with a **read-only** IAM user (only `dynamodb:Scan`/`Query`/`GetItem`/`DescribeTable` on that table ARN). No write actions exist in the code path.\n- The **FoV Lambda** reads the **lens** table with its own read-only execution role. For `compute_fov_catalog` full-catalog coverage the Lambda needs `ALLOW_LENS_SCAN=true` and `dynamodb:Scan` on the lens table.\n- Sensor table partition key is the part number (`id`); attributes used: `sensormfg`, `sensorhpix`, `sensorvpix`, `sensorpitch`, `sensortype` (shutter type).\n- Lens table partition key is the SKU; the Lambda's `LENS_PK` must be set accordingly.\n\n## Local development\n\nRequirements: Node.js 22+.\n\n```bash\nnpm install\nnpm run verify\nnpm run dev\n```\n\nLocal smoke test:\n\n```bash\ncurl http://localhost:8787/healthz\ncurl -X POST http://localhost:8787/mcp \\\n  -H 'content-type: application/json' \\\n  -H 'accept: application/json, text/event-stream' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}'\n```\n\n## Deploy\n\nRun verification first, then deploy through CI so `/healthz` receives production build metadata (`ENVIRONMENT=production`, package `VERSION`, and `GIT_SHA=$GITHUB_SHA`). The source `wrangler.toml` intentionally does not define deployable local metadata placeholders.\n\n```bash\nnpm run verify\nnpm run deploy:ci\n```\n\nFor an approved manual deploy, `npm run deploy` runs `scripts/deploy.mjs`,\nwhich deploys with `--keep-vars` and injects the same production build\nmetadata. `npm run deploy:raw` is the unwrapped Wrangler deploy command.\n",
  "bytes": 17419,
  "sha": "045efd2c238402b4d29f47d7e4f836c3c3cc37f17f8597d43b24ebc50b3ea377",
  "repo_slug": "commonlandsabbe/commonlands-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_commonlands_optics_mcp_e8f79fd7/readme"
}