MCP Tool Contract - Viator Experiences
This document defines the behavior, field requirements, invocation rules, and expected specifications for the Viator search_experiences and get_experience_details MCP tools. These instructions guide the model on when and how to use each tool, what inputs are required, and how to manage ambiguity.
Table of Contents
Access
To use the MCP server, your client IP address must be added to an IP whitelist - no API keys or tokens are required. Once your IP address is whitelisted, all requests from that IP are accepted without further authentication.
To request access, reach out to the Viator Experiences team via partnerapi@tripadvisor.com with your client IP address. Requests are typically processed within one business day.
If you receive a 403 Forbidden response, your IP has not yet been whitelisted or the request is coming from an unexpected address.
Base URL & Setup
The MCP server is available at:
https://exp-app-mcp.prod.ep.viator.com/mcp
Configure this as the server URL in your MCP client. Once your IP is whitelisted, no additional setup is required.
Tools
search_experiences
Retrieves a curated list of Viator experiences based on required trip context and travel dates.
Supports both low-intent ("things to do in London this weekend") and high-intent ("kid-friendly cooking classes in Rome in July") queries.
The model may re-issue refined search calls when the user adds additional criteria.
When to Use
Invoke this tool whenever the user is asking for activities, experiences, or things to do in a destination and enough context is available or can be clarified.
Examples that should invoke the tool:
- "What should I do in Barcelona in May?"
- "Find kid-friendly tours in Rome."
- "Show me things to do in London for under $50 this weekend."
When NOT to Use
Do NOT invoke the tool when the user:
- Asks about logistics (visas, weather, transportation),
- Requests hotel, restaurant, or flight recommendations,
- Asks about Viator booking support (refunds, cancellations),
- Refers to a landmark (Eiffel Tower) instead of a destination (Paris).
The model should ask clarifying questions when query or dates are missing or ambiguous.
Required Behavior Rules
- Pass the user's full query string as-is in
searchTerm: destination, category, and preferences can all be expressed there. startDateandendDatemust be ISO-8601. Convert natural language ("next weekend", "early April") to concrete dates before calling.limitshould never be used to interpret user intent. It affects output volume, not query meaning.
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
| searchTerm | string | Y | The full user search query string. Destination, preferences, and activity type can all be expressed here (e.g. "kid-friendly cooking classes in Rome"). |
| startDate | string | Y | First available date in ISO-8601 format (YYYY-MM-DD). LLM must convert natural language to a concrete date. |
| endDate | string | N | Last available date in ISO-8601 format (YYYY-MM-DD). Defaults to startDate if the user provides a single-day query. |
| duration | integer | N | Maximum experience duration in minutes. |
| fromPrice | number | N | Lowest per person price. |
| toPrice | number | N | Highest per person price. |
| currency | string | N | Currency code for price filtering and display (e.g. USD, EUR). |
| locale | string | N | Language/region code (e.g. en-US). Defaults to en-US. |
| limit | integer | N | Number of products to return (1-10). Defaults to 5. |
| sessionId | string | Y | Session identifier. Generate on first call and reuse throughout the session. |
Request example
{
"searchTerm": "kid-friendly things to do in Paris",
"startDate": "2025-06-01",
"endDate": "2025-06-07",
"limit": 5,
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}
Response Fields
The tool returns a JSON object with:
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Y | Conversation identifier; echoes the request sessionId when provided, otherwise a server-generated value to reuse on follow-up calls. |
| experiences | array | Y | List of experience summaries. Each element has: |
| experiences[].title | string | Y | Human-readable name of the experience. |
| experiences[].code | string | Y | Experience internal code. Pass this to get_experience_details for full detail. |
| experiences[].thumbnail | string | Y | URL of the lead image. To be used in product cards or lists. |
| experiences[].freeCancellation | boolean | Y | Whether free cancellation is available for this experience. |
| experiences[].fromPrice | number | Y | Advertised per-person starting price in the search currency context. |
| experiences[].fromPriceBeforeDiscount | number | N | Strikethrough or pre-discount price when shown; may be omitted or null. |
| experiences[].clickOffToLander | string | Y | Outbound URL to the Viator lander for this experience. |
| experiences[].rating | number | N | Average customer rating when the API supplies it; may be omitted or null. |
| experiences[].reviewCount | number | N | Total review count when available; may be omitted or null. |
| experiences[].duration | object | N | Length hints in minutes; may be omitted or null. When present, each sub-field may still be null: |
| experiences[].duration.fixedDurationInMinutes | number | N | Fixed duration for single-length experiences. |
| experiences[].duration.variableDurationFromMinutes | number | N | Lower bound when duration is a range. |
| experiences[].duration.variableDurationToMinutes | number | N | Upper bound when duration is a range. |
| experiences[].keyAttributes | object | N | Display-oriented experience metadata. May be omitted or null. |
| experiences[].keyAttributes.features | array | N | Feature tags (e.g. KID_FRIENDLY). |
| experiences[].keyAttributes.mainCategory | string | N | Primary category label for the experience when returned. |
Response example
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"experiences": [
{
"title": "Example Walking Tour",
"code": "12345P7",
"thumbnail": "https://example.com/image.jpg",
"rating": 4.8,
"reviewCount": 1200,
"freeCancellation": true,
"fromPrice": 49.0,
"fromPriceBeforeDiscount": 59.0,
"clickOffToLander": "https://example.com/lander",
"duration": {
"fixedDurationInMinutes": 120,
"variableDurationFromMinutes": null,
"variableDurationToMinutes": null
},
"keyAttributes": {
"features": ["KID_FRIENDLY"],
"mainCategory": "Walking Tours"
}
}
]
}
get_experience_details
Retrieves enriched details for a specific Viator product, including description, highlights, images, inclusions/exclusions, insider tips, pricing, and booking URL.
Used when the user selects a product or explicitly asks for more information about one previously shown.
When to Use
Invoke this tool when the user:
- Clicks "See more" (or equivalent) in UI
- Says "Tell me more about..." or similar
- Explicitly asks for details on a previously shown experience
If the user's reference is ambiguous (e.g. "the museum tour" after showing several), ask which one they mean before calling.
When NOT to Use
- Discovering new products - use
search_experiencesfor that - Any product not previously returned by a search.
codemust come from previous search results. Never invent or guess IDs.
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | Y | Unique identifier of the Viator experience. Must be previously returned by search_experiences. |
| locale | string | N | Language/region code (e.g. en-US). Defaults to en-US unless the user specifies or context implies another. |
| sessionId | string | Y | A unique identifier for the current user-session. Re-used from the search_experiences response. |
Request example
{
"code": "12345P7",
"locale": "en-US",
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}
Response Fields
The tool returns a JSON object with:
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Y | Conversation identifier; echoes the request sessionId when provided, otherwise a server-generated value to reuse on follow-up calls. |
| experienceDetails | object | Y | Detailed data for the selected experience. |
| experienceDetails.code | string | Y | Experience internal code. |
| experienceDetails.title | string | Y | Human-readable name of the experience. |
| experienceDetails.thumbnail | string | Y | URL of the lead image. To be used in product cards or lists. |
| experienceDetails.freeCancellation | boolean | Y | Whether free cancellation is available for this experience. |
| experienceDetails.fromPrice | number | Y | Advertised per-person starting price in the search currency context. |
| experienceDetails.fromPriceBeforeDiscount | number | N | Strikethrough or pre-discount price when shown; may be omitted or null. |
| experienceDetails.clickOffToPDP | string | Y | Outbound URL to the Viator lander for this experience. |
| experienceDetails.rating | number | N | Average customer rating when the API supplies it; may be omitted or null. |
| experienceDetails.reviewCount | number | N | Total review count when available; may be omitted or null. |
| experienceDetails.duration | object | N | Length hints in minutes; may be omitted or null. When present, each sub-field may still be null: |
| experienceDetails.duration.fixedDurationInMinutes | number | N | Fixed duration for single-length experiences. |
| experienceDetails.duration.variableDurationFromMinutes | number | N | Lower bound when duration is a range. |
| experienceDetails.duration.variableDurationToMinutes | number | N | Upper bound when duration is a range. |
| experienceDetails.keyAttributes | object | N | Display-oriented experience metadata. May be omitted or null. |
| experienceDetails.keyAttributes.features | array | N | Feature tags (e.g. KID_FRIENDLY). |
| experienceDetails.keyAttributes.mainCategory | string | N | Primary category label for the experience when returned. |
| experienceDetails.description | string | N | Main experience description text. |
| experienceDetails.insiderTips | string | N | Extra tips when available. May be omitted or not. |
Response example
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"experienceDetails": {
"code": "12345P7",
"title": "Example Walking Tour",
"description": "Explore the city with a local expert guide.",
"insiderTips": "Wear comfortable shoes.",
"thumbnail": "https://example.com/image.jpg",
"rating": 4.8,
"reviewCount": 1200,
"freeCancellation": true,
"clickOffToPDP": "https://example.com/pdp"
}
}
Error & Ambiguity Handling
Error Handling Rules
- Most errors will be shown as regular MCP errors (inside of a HTTP 200 response), with the standard "isError: true" and the text field being the human readable error message for which the MCP Client must react appropriately
- The only exceptions are the 403 Forbidden responses if your IP address is not whitelisted.
Empty results behaviour
If the tool returns zero results, the model must proactively guide the user toward a successful refinement. It should review the parameters used in the failed request and recommend the most appropriate based on the proposed logic below:
- If the query requests a specific feature (eg private tour), suggest removing this constraint. Eg: "Private tours may be limited in this area, would you like to consider group options too?"
- If duration provided, suggest trying a shorter or longer duration.
- If price constraint was applied, suggest widening (or removing) price limits.
- Else, suggest a broader location search. Eg: "Nothing matches central Siena, should we try Tuscany or nearby towns?"
Rate Limiting
The API enforces rate limits to ensure reliable service across all consumers. Limits are generous and designed to accommodate standard conversational usage patterns, most integrations won't need to think about this at all.
If you do hit a limit, the server responds with the error message “Rate limit exceeded. Please retry after X seconds." Exponential backoff is also a safe default strategy.
If your use case involves unusually high call volumes, please reach out to the team before going live.
End-to-end example
Here's a complete flow from a user message through to a detail request:
User: "Find me kid-friendly things to do in Paris next weekend."
Step 1: Model resolves the date (assuming today is 2025-05-28): "next weekend" = startDate: 2025-06-07, endDate: 2025-06-08.
Step 2: Call search_experiences
{
"searchTerm": "kid-friendly things to do in Paris next weekend",
"startDate": "2025-06-07",
"endDate": "2025-06-08",
"limit": 5
}
Step 3: Server returns results including (among others):
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"experiences": [
{
"title": "Paris Catacombs Skip-the-Line Tour for Families",
"code": "12345P7",
"rating": 4.8,
"reviewCount": 1200,
"freeCancellation": true,
"fromPrice": 49.0,
"clickOffToLander": "https://viator.com/..."
}
]
}
User: "Tell me more about the Catacombs tour."
Step 4: Call get_experience_details
{
"code": "12345P7",
"locale": "en-US",
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}
Step 5: Server returns enriched product detail, which the model uses to present highlights, inclusions, pricing, and a booking link to the user.