> ## Documentation Index
> Fetch the complete documentation index at: https://docs.skinai.co/llms.txt
> Use this file to discover all available pages before exploring further.

# recommend_products

> Score catalog products against a completed scan.

Maps the scan's scores to cosmetic concerns and scores the catalog against them. It uses the same matching engine as the Skan app, so a `match_score` here equals the one the app shows for the same scan and product. Filters are optional. Pass `skin_type` only if the person stated it; it is never inferred from the scan.

`match_score` is a cosmetic fit estimate from 0 to 100. It is not a diagnosis or a promise of results. No prices, stock or retailer links are returned.

**Allowance:** 1 match per scan. After that the tool succeeds with `products: []` and `entitlement.upgrade_required: true`.

**Common errors:** `scan_required`, `scan_not_ready`, `scan_not_uploaded`, `not_found`, `catalog_unavailable`, `rate_limited`.

|             |                                                                      |
| ----------- | -------------------------------------------------------------------- |
| Tool        | `recommend_products`                                                 |
| Listed on   | `/mcp` (guest) and `/mcp/account` (OAuth)                            |
| Annotations | `readOnlyHint: false` `destructiveHint: false` `openWorldHint: true` |

## Input

Sent as `params.arguments` of `tools/call`. Unknown keys are rejected with `invalid_input`.

<ParamField body="run_id" type="string">
  Required. The completed scan to match against. Without it this returns scan\_required.

  Format `uuid`.
</ParamField>

<ParamField body="access_token" type="string">
  Required. That scan's private access token.

  Pattern `^[a-f0-9]{64}$`.
</ParamField>

<ParamField body="product_type" type="enum<string>">
  Narrow the match to one kind of product.

  One of `cleanser`, `toner`, `serum`, `moisturizer`, `sunscreen`, `eye_care`, `exfoliator`, `mask`, `treatment`.
</ParamField>

<ParamField body="skin_type" type="enum<string>">
  Only if the person stated it. Never inferred from the scan.

  One of `dry`, `oily`, `combination`, `normal`.
</ParamField>

<ParamField body="free_from" type="enum<string>[]">
  Max 3 items. Items: `fragrance`, `alcohol`, `fungal_acne_triggers`.
</ParamField>

<ParamField body="vegan" type="boolean" />

<ParamField body="cruelty_free" type="boolean" />

<ParamField body="brand" type="string">
  Max length 60.
</ParamField>

<ParamField body="country" type="string">
  Max length 40.
</ParamField>

## Output

Returned as `structuredContent`, and as the same object serialised in `content[0].text`. On failure the result has `isError: true` and an [error body](/errors) instead.

<ResponseField name="products" type="object[]" required>
  Max 5 items.

  <Expandable title="item">
    <ResponseField name="product_id" type="string" required />

    <ResponseField name="name" type="string" required />

    <ResponseField name="brand" type="string | null" required />

    <ResponseField name="product_type" type="string | null" required />

    <ResponseField name="match_score" type="integer | null" required>
      Range 0–100.
    </ResponseField>

    <ResponseField name="targets" type="string[]" required>
      Max 6 items.
    </ResponseField>

    <ResponseField name="matches_concerns" type="string[]" required>
      Max 6 items.
    </ResponseField>

    <ResponseField name="key_ingredients" type="string[]" required>
      Max 5 items.
    </ResponseField>

    <ResponseField name="attributes" type="object" required>
      <Expandable title="properties">
        <ResponseField name="vegan" type="boolean | null" required />

        <ResponseField name="cruelty_free" type="boolean | null" required />

        <ResponseField name="fragrance_free" type="boolean | null" required />

        <ResponseField name="alcohol_free" type="boolean | null" required />

        <ResponseField name="fungal_acne_safe" type="boolean | null" required />
      </Expandable>
    </ResponseField>

    <ResponseField name="country" type="string | null" required />

    <ResponseField name="image_url" type="string | null" required>
      Format `uri`.
    </ResponseField>

    <ResponseField name="product_url" type="string | null" required>
      Format `uri`.
    </ResponseField>

    <ResponseField name="buy_url" type="string | null" required>
      Format `uri`.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="scan_concerns" type="string[]" required>
  Max 6 items.
</ResponseField>

<ResponseField name="match_basis" type="string" required />

<ResponseField name="entitlement" type="object" required>
  <Expandable title="properties">
    <ResponseField name="tier" type="&#x22;free_guest&#x22;" required />

    <ResponseField name="used" type="integer" required>
      Range 0–9007199254740991.
    </ResponseField>

    <ResponseField name="included" type="integer" required>
      Range 0–9007199254740991.
    </ResponseField>

    <ResponseField name="remaining" type="integer" required>
      Range 0–9007199254740991.
    </ResponseField>

    <ResponseField name="upgrade_required" type="boolean" required />

    <ResponseField name="more_in_app" type="string" required />
  </Expandable>
</ResponseField>

<ResponseField name="limitations" type="string" required />

<AccordionGroup>
  <Accordion title="Description the model receives">
    ```text theme={null}
    Recommend skincare products for a completed Skan scan. Needs that scan's run_id and access_token. Returns the product name, brand, what it is formulated to target, and the Skan match percentage, scored with the same engine the Skan app uses. Optionally narrow by product type or stated preferences; never guess a skin type or a medical condition on the person's behalf. The free guest allowance is one match per scan: when entitlement.upgrade_required is true, say the rest is in the Skan app and do not start another scan to get around it. Report the match percentage as a cosmetic fit estimate, never as a diagnosis, a cure or a promise of results. Completes in one call.
    ```
  </Accordion>

  <Accordion title="Input JSON Schema">
    ```json theme={null}
    {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "run_id": {
          "description": "Required. The completed scan to match against. Without it this returns scan_required.",
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        "access_token": {
          "description": "Required. That scan's private access token.",
          "type": "string",
          "pattern": "^[a-f0-9]{64}$"
        },
        "product_type": {
          "description": "Narrow the match to one kind of product.",
          "type": "string",
          "enum": [
            "cleanser",
            "toner",
            "serum",
            "moisturizer",
            "sunscreen",
            "eye_care",
            "exfoliator",
            "mask",
            "treatment"
          ]
        },
        "skin_type": {
          "description": "Only if the person stated it. Never inferred from the scan.",
          "type": "string",
          "enum": [
            "dry",
            "oily",
            "combination",
            "normal"
          ]
        },
        "free_from": {
          "maxItems": 3,
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "fragrance",
              "alcohol",
              "fungal_acne_triggers"
            ]
          }
        },
        "vegan": {
          "type": "boolean"
        },
        "cruelty_free": {
          "type": "boolean"
        },
        "brand": {
          "type": "string",
          "maxLength": 60
        },
        "country": {
          "type": "string",
          "maxLength": 40
        }
      },
      "additionalProperties": false
    }
    ```
  </Accordion>

  <Accordion title="Output JSON Schema">
    ```json theme={null}
    {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "products": {
          "maxItems": 5,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "product_id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "brand": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "product_type": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "match_score": {
                "anyOf": [
                  {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "targets": {
                "maxItems": 6,
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "matches_concerns": {
                "maxItems": 6,
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "key_ingredients": {
                "maxItems": 5,
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "vegan": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  },
                  "cruelty_free": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  },
                  "fragrance_free": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  },
                  "alcohol_free": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  },
                  "fungal_acne_safe": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  }
                },
                "required": [
                  "vegan",
                  "cruelty_free",
                  "fragrance_free",
                  "alcohol_free",
                  "fungal_acne_safe"
                ],
                "additionalProperties": false
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "image_url": {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "product_url": {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "buy_url": {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "product_id",
              "name",
              "brand",
              "product_type",
              "match_score",
              "targets",
              "matches_concerns",
              "key_ingredients",
              "attributes",
              "country",
              "image_url",
              "product_url",
              "buy_url"
            ],
            "additionalProperties": false
          }
        },
        "scan_concerns": {
          "maxItems": 6,
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "match_basis": {
          "type": "string"
        },
        "entitlement": {
          "type": "object",
          "properties": {
            "tier": {
              "type": "string",
              "const": "free_guest"
            },
            "used": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "included": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "remaining": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "upgrade_required": {
              "type": "boolean"
            },
            "more_in_app": {
              "type": "string"
            }
          },
          "required": [
            "tier",
            "used",
            "included",
            "remaining",
            "upgrade_required",
            "more_in_app"
          ],
          "additionalProperties": false
        },
        "limitations": {
          "type": "string"
        }
      },
      "required": [
        "products",
        "scan_concerns",
        "match_basis",
        "entitlement",
        "limitations"
      ],
      "additionalProperties": false
    }
    ```
  </Accordion>
</AccordionGroup>

<RequestExample>
  ```json Request theme={null}
  {
    "jsonrpc": "2.0", "id": 4, "method": "tools/call",
    "params": {
      "name": "recommend_products",
      "arguments": {
        "run_id": "db56607f-edeb-47f3-bc5d-812d7af19adb",
        "access_token": "<access_token>",
        "product_type": "serum",
        "free_from": ["fragrance"]
      }
    }
  }
  ```
</RequestExample>

<ResponseExample>
  ```json Response (structuredContent) theme={null}
  {
    "products": [
      {
        "product_id": "products_v2:218394",
        "name": "Bakuchiol Mela Free Serum",
        "brand": "Raviel",
        "product_type": "Serum",
        "match_score": 86,
        "targets": ["hydration", "soothing", "brightening", "acne and pores", "antioxidant support"],
        "matches_concerns": ["soothing"],
        "key_ingredients": [],
        "attributes": { "vegan": true, "cruelty_free": false, "fragrance_free": true, "alcohol_free": true, "fungal_acne_safe": false },
        "country": "South Korea",
        "image_url": "https://imagedelivery.net/…/270903/public",
        "product_url": null,
        "buy_url": null
      }
    ],
    "scan_concerns": ["sensitivity", "eye-bags"],
    "match_basis": "Scored against the concerns derived from this scan, using the same matching Skan's app uses. …",
    "entitlement": { "tier": "free_guest", "used": 1, "included": 1, "remaining": 0, "upgrade_required": false, "more_in_app": "…" },
    "limitations": "The match percentage compares this product's formulation against the concerns derived from this scan, …"
  }
  ```
</ResponseExample>
