iflow-mcp_particular-audience-mcp-search-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Particular Audience.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: iflow-mcp_particular-audience-mcp-search-server
3
+ Version: 0.1.0
4
+ Summary: MCP Search Server
5
+ Requires-Python: >=3.11
6
+ License-File: LICENSE
7
+ Requires-Dist: fastapi>=0.95.0
8
+ Requires-Dist: uvicorn>=0.21.0
9
+ Requires-Dist: requests>=2.28.0
10
+ Requires-Dist: pydantic>=1.10.7
11
+ Requires-Dist: mcp>=0.5.0
12
+ Requires-Dist: mcp-use>=1.2.0
13
+ Requires-Dist: fastembed>=0.4.2
14
+ Requires-Dist: python-dotenv>=1.0.0
15
+ Requires-Dist: gql>=3.0.0
16
+ Requires-Dist: requests-toolbelt>=1.0.0
17
+ Dynamic: license-file
@@ -0,0 +1,252 @@
1
+ # Adaptive Transformer Search MCP Server
2
+
3
+ This MCP (Model Context Protocol) server provides access to [Particular Audience's Adaptive Transformer Search (ATS)](https://particularaudience.com/search/) - an AI-powered eCommerce search solution that harnesses the power of Large Language Models to understand customer search intent and eliminate zero search results.
4
+
5
+ ## What is Adaptive Transformer Search?
6
+
7
+ Adaptive Transformer Search (ATS) represents a revolutionary leap beyond traditional keyword-based search and basic catalog browsing. Unlike conventional search that relies on exact token matching, ATS uses the same transformer technology behind OpenAI's GPT and Google's translation systems to understand the semantic meaning and intent behind customer queries. This enables the system to understand the difference between "chocolate milk" and "milk chocolate," handle natural language queries, and provide relevant results even when exact keyword matches don't exist.
8
+
9
+ ### Benefits Over Traditional Search
10
+
11
+ ATS addresses the $300 billion search problem in eCommerce by eliminating the need for manual synonym rules, redirects, and ongoing search maintenance. Traditional keyword search requires extensive manual configuration and often fails when customers use natural language or misspellings. ATS automatically understands customer intent, reducing zero search results by up to 70% while increasing search revenue by 20% and eliminating 99% of manual search management work.
12
+
13
+ ### Merchandising Control for Retailers
14
+
15
+ One of the key advantages of ATS is the ability for retailers to maintain merchandising control over how LLMs interpret and rank search results. Retailers can boost specific products, brands, or categories within search results based on promotional campaigns, inventory levels, or margin objectives. This ensures that while the AI provides intelligent, relevant results, retailers retain the ability to influence discovery in alignment with their business goals.
16
+
17
+ ### Enabling Retail Media in LLM Discovery
18
+
19
+ ATS seamlessly integrates retail media capabilities into the search experience, allowing sponsored products to be intelligently woven into search results. This creates new revenue opportunities for retailers while maintaining relevance and user experience. The system can prioritize sponsored products when they genuinely match customer intent, creating a win-win scenario for retailers, advertisers, and customers.
20
+
21
+ ## Features
22
+
23
+ This MCP server provides three main search tools that interface with the Particular Audience Search API, giving you access to all ATS capabilities:
24
+
25
+ - **Search**: Adaptive Transformer Search with natural language understanding, optional filters, and pagination
26
+ - **Filtered Search**: ATS search with required filters to narrow results while maintaining semantic understanding
27
+ - **Sorted Search**: ATS search with custom sorting options (price, popularity, etc.) and merchandising control
28
+
29
+ The server handles authentication automatically and provides comprehensive error handling and retry logic. All searches benefit from ATS's transformer technology, eliminating zero search results and providing intelligent, relevant product matches.
30
+
31
+ ## ATS Capabilities Available Through This MCP Server
32
+
33
+ When you use this MCP server, you get access to all Adaptive Transformer Search capabilities:
34
+
35
+ ### Natural Language Understanding
36
+ - Understands semantic meaning behind queries (e.g., "chocolate milk" vs "milk chocolate")
37
+ - Handles natural language queries like "comfortable shoes for walking under $100"
38
+ - Processes misspellings and variations automatically
39
+
40
+ ### Intelligent Search Results
41
+ - Eliminates zero search results by up to 70%
42
+ - Provides relevant results even when exact keyword matches don't exist
43
+ - Understands product relationships and context
44
+
45
+ ### Merchandising Control
46
+ - Boost specific products, brands, or categories in search results
47
+ - Control ranking based on promotional campaigns, inventory, or margin objectives
48
+ - Maintain business control while leveraging AI intelligence
49
+
50
+ ### Retail Media Integration
51
+ - Seamlessly integrate sponsored products into search results
52
+ - Maintain relevance while creating revenue opportunities
53
+ - Intelligent placement of retail media content
54
+
55
+ ### Zero Manual Maintenance
56
+ - No need for synonym rules, redirects, or manual search configuration
57
+ - Automatically adapts to changes in user behavior and catalog data
58
+ - Reduces manual search management work by 99%
59
+
60
+ ## Connecting to Onsite LLM/Chat Agents
61
+
62
+ This MCP server enables seamless integration of ATS into your existing LLM-powered chat agents and discovery experiences. By connecting this server to your onsite AI assistant, customers can now search your product catalog using natural language through chat interfaces. The AI can understand complex queries like "show me comfortable shoes for walking that are under $100" and return relevant results, even if the customer doesn't use exact product names or categories. This creates a more intuitive, conversational shopping experience that feels natural to customers while driving higher conversion rates.
63
+
64
+ ## Prerequisites
65
+
66
+ - Python 3.11 or higher
67
+ - [UV](https://github.com/astral-sh/uv) package manager
68
+ - Docker (optional, for containerized deployment)
69
+
70
+ ## Configuration
71
+
72
+ Create a `.env` file with the following content (or set environment variables directly):
73
+
74
+ ```
75
+ # Authentication settings
76
+ AUTH_ENDPOINT=AUTHENTICATION_ENDPOINT
77
+ SEARCH_API_ENDPOINT=SEARCH_ENDPOINT
78
+ CLIENT_ID=your_client_id_here
79
+ CLIENT_SHORTCODE=your_client_shortcode_here
80
+ CLIENT_SECRET=your_client_secret_here
81
+
82
+ # Server settings
83
+ HOST=0.0.0.0
84
+ PORT=3000
85
+ MESSAGE_PATH=/mcp/messages/
86
+ ```
87
+
88
+ ## Deployment Options
89
+
90
+ ### Option 1: Direct UV Run
91
+
92
+ Run directly with UV (assuming UV is pre-installed):
93
+
94
+ ```bash
95
+ # Simple run
96
+ uv run mcp_search_server.py
97
+
98
+ # Or with inline environment variables
99
+ AUTH_ENDPOINT=AUTHENTICATION_ENDPOINT \
100
+ SEARCH_API_ENDPOINT=SEARCH_ENDPOINT \
101
+ CLIENT_ID=your_id \
102
+ CLIENT_SHORTCODE=your_code \
103
+ CLIENT_SECRET=your_secret \
104
+ uv run mcp_search_server.py
105
+ ```
106
+
107
+ ### Option 2: Docker Deployment
108
+
109
+ Build and run using Docker:
110
+
111
+ ```bash
112
+ # Build
113
+ docker build -t mcp-search-server .
114
+
115
+ # Run with env file
116
+ docker run -p 3000:3000 --env-file .env mcp-search-server
117
+
118
+ # Or run with inline environment variables
119
+ docker run -p 3000:3000 \
120
+ -e AUTH_ENDPOINT=AUTHENTICATION_ENDPOINT \
121
+ -e SEARCH_API_ENDPOINT=SEARCH_ENDPOINT \
122
+ -e CLIENT_ID=your_id \
123
+ -e CLIENT_SHORTCODE=your_code \
124
+ -e CLIENT_SECRET=your_secret \
125
+ mcp-search-server
126
+ ```
127
+
128
+ ## MCP Client Configuration
129
+
130
+ You can configure AI assistants like Claude Desktop or VS Code Copilot to use this MCP Search Server.
131
+
132
+ ### Integration with Claude Desktop
133
+
134
+ To configure Claude Desktop:
135
+
136
+ 1. Specify your API credentials
137
+ 2. Retrieve your `uv` command full path (e.g. `which uv`)
138
+ 3. Edit the Claude Desktop configuration file (location varies by OS:
139
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
140
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
141
+ - Linux: `~/.config/Claude/claude_desktop_config.json`)
142
+
143
+ #### Local UV Configuration
144
+
145
+ ```json
146
+ {
147
+ "mcpServers": {
148
+ "uv-search-server": {
149
+ "command": "/path/to/your/uv",
150
+ "args": [
151
+ "--directory",
152
+ "/path/to/your/project/directory",
153
+ "run",
154
+ "mcp_search_server.py"
155
+ ],
156
+ "env": {
157
+ "AUTH_ENDPOINT": "AUTHENTICATION_ENDPOINT",
158
+ "SEARCH_API_ENDPOINT": "SEARCH_ENDPOINT",
159
+ "CLIENT_ID": "your_client_id",
160
+ "CLIENT_SHORTCODE": "your_client_shortcode",
161
+ "CLIENT_SECRET": "your_client_secret",
162
+ "PYTHONUNBUFFERED": "1"
163
+ }
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ #### Docker Configuration
170
+
171
+ ```json
172
+ {
173
+ "mcpServers": {
174
+ "uv-search-server": {
175
+ "command": "docker",
176
+ "args": [
177
+ "run",
178
+ "--rm",
179
+ "--name",
180
+ "mcp-search-server",
181
+ "-p",
182
+ "3000:3000",
183
+ "-i",
184
+ "-e", "AUTH_ENDPOINT",
185
+ "-e", "SEARCH_API_ENDPOINT",
186
+ "-e", "CLIENT_ID",
187
+ "-e", "CLIENT_SHORTCODE",
188
+ "-e", "CLIENT_SECRET",
189
+ "-e", "PYTHONUNBUFFERED",
190
+ "mcp-search-server"
191
+ ],
192
+ "env": {
193
+ "AUTH_ENDPOINT": "AUTHENTICATION_ENDPOINT",
194
+ "SEARCH_API_ENDPOINT": "SEARCH_ENDPOINT",
195
+ "CLIENT_ID": "your_client_id",
196
+ "CLIENT_SHORTCODE": "your_client_shortcode",
197
+ "CLIENT_SECRET": "your_client_secret",
198
+ "PYTHONUNBUFFERED": "1"
199
+ }
200
+ }
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### Integration with VS Code
206
+
207
+ For VS Code integration:
208
+
209
+ 1. Enable agent mode tools in your `settings.json`:
210
+ ```json
211
+ {
212
+ "chat.agent.enabled": true
213
+ }
214
+ ```
215
+
216
+ 2. Configure the Search Server in your `.vscode/mcp.json` or in VS Code's `settings.json`:
217
+ ```json
218
+ // Example .vscode/mcp.json
219
+ {
220
+ "servers": {
221
+ "uv-search-server": {
222
+ "type": "stdio",
223
+ "command": "/path/to/your/uv",
224
+ "args": [
225
+ "--directory",
226
+ "/path/to/your/project/directory",
227
+ "run",
228
+ "mcp_search_server.py"
229
+ ],
230
+ "env": {
231
+ "AUTH_ENDPOINT": "AUTHENTICATION_ENDPOINT",
232
+ "SEARCH_API_ENDPOINT": "SEARCH_ENDPOINT",
233
+ "CLIENT_ID": "your_client_id",
234
+ "CLIENT_SHORTCODE": "your_client_shortcode",
235
+ "CLIENT_SECRET": "your_client_secret",
236
+ "PYTHONUNBUFFERED": "1"
237
+ }
238
+ }
239
+ }
240
+ }
241
+ ```
242
+
243
+ ### Troubleshooting
244
+
245
+ For Claude Desktop, you can check logs with:
246
+ ```bash
247
+ tail -f ~/Library/Logs/Claude/mcp-server-uv-search-server.log
248
+ ```
249
+
250
+ ## License
251
+
252
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: iflow-mcp_particular-audience-mcp-search-server
3
+ Version: 0.1.0
4
+ Summary: MCP Search Server
5
+ Requires-Python: >=3.11
6
+ License-File: LICENSE
7
+ Requires-Dist: fastapi>=0.95.0
8
+ Requires-Dist: uvicorn>=0.21.0
9
+ Requires-Dist: requests>=2.28.0
10
+ Requires-Dist: pydantic>=1.10.7
11
+ Requires-Dist: mcp>=0.5.0
12
+ Requires-Dist: mcp-use>=1.2.0
13
+ Requires-Dist: fastembed>=0.4.2
14
+ Requires-Dist: python-dotenv>=1.0.0
15
+ Requires-Dist: gql>=3.0.0
16
+ Requires-Dist: requests-toolbelt>=1.0.0
17
+ Dynamic: license-file
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ mcp_search_server.py
4
+ pyproject.toml
5
+ iflow_mcp_particular_audience_mcp_search_server.egg-info/PKG-INFO
6
+ iflow_mcp_particular_audience_mcp_search_server.egg-info/SOURCES.txt
7
+ iflow_mcp_particular_audience_mcp_search_server.egg-info/dependency_links.txt
8
+ iflow_mcp_particular_audience_mcp_search_server.egg-info/requires.txt
9
+ iflow_mcp_particular_audience_mcp_search_server.egg-info/top_level.txt
@@ -0,0 +1,10 @@
1
+ fastapi>=0.95.0
2
+ uvicorn>=0.21.0
3
+ requests>=2.28.0
4
+ pydantic>=1.10.7
5
+ mcp>=0.5.0
6
+ mcp-use>=1.2.0
7
+ fastembed>=0.4.2
8
+ python-dotenv>=1.0.0
9
+ gql>=3.0.0
10
+ requests-toolbelt>=1.0.0
@@ -0,0 +1,788 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Search Server
4
+
5
+ This MCP server provides tools for searching products using the Particular Audience Search API.
6
+ The server exposes three main tools:
7
+ - search: Basic product search with optional filters
8
+ - filtered_search: Search with required filters
9
+ - sorted_search: Search with custom sorting options
10
+
11
+ Usage:
12
+ python mcp_search_server.py
13
+
14
+ Configuration:
15
+ Set the following environment variables in a .env file:
16
+ - AUTH_ENDPOINT: Authentication endpoint
17
+ - SEARCH_API_ENDPOINT: Search API endpoint
18
+ - CLIENT_ID: Client ID for authentication (required)
19
+ - CLIENT_SHORTCODE: Client shortcode (required)
20
+ - CLIENT_SECRET: Client secret for authentication (required)
21
+ - HOST: Server host (default: 0.0.0.0)
22
+ - PORT: Server port (default: 3000)
23
+ - MESSAGE_PATH: Path for MCP messages (default: /mcp/messages/)
24
+ """
25
+
26
+ import os
27
+ import time
28
+ import logging
29
+ import json
30
+ from typing import Dict, Any, List, Optional
31
+ import traceback
32
+ from contextlib import asynccontextmanager
33
+
34
+ import requests
35
+ from fastapi import HTTPException
36
+ from pydantic import BaseModel, Field
37
+ from mcp.server.fastmcp import FastMCP, Context
38
+
39
+ # Import dotenv for environment variable loading
40
+ from dotenv import load_dotenv
41
+
42
+ # Load environment variables from .env file
43
+ load_dotenv()
44
+
45
+ # Configure logging
46
+ logging.basicConfig(
47
+ level=logging.INFO,
48
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
49
+ )
50
+ logger = logging.getLogger("mcp_search_server")
51
+
52
+ # Service configuration from environment variables
53
+ AUTH_ENDPOINT = os.environ.get("AUTH_ENDPOINT")
54
+ SEARCH_API_ENDPOINT = os.environ.get("SEARCH_API_ENDPOINT")
55
+ CLIENT_ID = os.environ.get("CLIENT_ID")
56
+ CLIENT_SHORTCODE = os.environ.get("CLIENT_SHORTCODE")
57
+ CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
58
+
59
+ # Server configuration from environment variables
60
+ HOST = os.environ.get("HOST", "0.0.0.0")
61
+ PORT = int(os.environ.get("PORT", 3000))
62
+ MESSAGE_PATH = os.environ.get("MESSAGE_PATH", "/mcp/messages/")
63
+
64
+ # Token cache
65
+ token_cache = {}
66
+
67
+ # Validate required environment variables
68
+ if not CLIENT_ID:
69
+ raise ValueError("CLIENT_ID environment variable is required")
70
+ if not CLIENT_SHORTCODE:
71
+ raise ValueError("CLIENT_SHORTCODE environment variable is required")
72
+ if not CLIENT_SECRET:
73
+ raise ValueError("CLIENT_SECRET environment variable is required")
74
+
75
+ # Core models
76
+ class FilterSpec(BaseModel):
77
+ field: str
78
+ value: Any
79
+ operator: str = "eq"
80
+
81
+ class SortSpec(BaseModel):
82
+ field: str
83
+ order: str = "desc"
84
+ type: str = "number"
85
+
86
+ # Search response models
87
+ class PaginationInfo(BaseModel):
88
+ current_page: int
89
+ total_pages: int
90
+ total_results: int
91
+ page_size: int
92
+
93
+ class SearchResponse(BaseModel):
94
+ results: List[Dict[str, Any]]
95
+ pagination: PaginationInfo
96
+ aggregations: Optional[Dict[str, Any]] = None
97
+ suggestions: Optional[List[str]] = None
98
+ redirect_url: Optional[Dict[str, Any]] = None
99
+ execution_time_ms: Optional[int] = None
100
+
101
+ # Auth function
102
+ async def get_auth_token(client_id: str = None) -> str:
103
+ """
104
+ Get authentication token from the auth service.
105
+
106
+ Parameters:
107
+ - client_id: Optional client ID, defaults to CLIENT_ID from environment
108
+
109
+ Returns:
110
+ - Access token string
111
+
112
+ Raises:
113
+ - HTTPException: If authentication fails
114
+ """
115
+ client_id = client_id or CLIENT_ID
116
+ client_secret = CLIENT_SECRET
117
+
118
+ if not client_id:
119
+ raise HTTPException(status_code=400, detail="Client ID is required")
120
+
121
+ current_time = time.time()
122
+
123
+ # Use cached token if valid
124
+ if client_id in token_cache and token_cache[client_id].get("expires_at", 0) > current_time:
125
+ return token_cache[client_id]["access_token"]
126
+
127
+ # Fetch new token
128
+ logger.info(f"Fetching new auth token for client {client_id}")
129
+ try:
130
+ form_data = {
131
+ 'client_id': client_id,
132
+ 'grant_type': 'client_credentials'
133
+ }
134
+
135
+ if client_secret:
136
+ form_data['client_secret'] = client_secret
137
+
138
+ response = requests.post(
139
+ AUTH_ENDPOINT,
140
+ data=form_data,
141
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
142
+ timeout=10
143
+ )
144
+ response.raise_for_status()
145
+
146
+ data = response.json()
147
+ access_token = data.get("access_token")
148
+ expires_in = data.get("expires_in", 3600)
149
+ token_type = data.get("token_type", "Bearer")
150
+
151
+ # Cache token with 5-min safety margin
152
+ token_cache[client_id] = {
153
+ "access_token": access_token,
154
+ "token_type": token_type,
155
+ "expires_at": current_time + expires_in - 300
156
+ }
157
+
158
+ return access_token
159
+
160
+ except Exception as e:
161
+ logger.error(f"Auth token request failed: {e}")
162
+ raise HTTPException(status_code=500, detail=f"Failed to get auth token: {e}")
163
+
164
+ # Search API function
165
+ async def perform_search(
166
+ query: str,
167
+ start: int = 0,
168
+ size: int = 20,
169
+ filters: List[FilterSpec] = None,
170
+ sort_fields: List[SortSpec] = None
171
+ ) -> SearchResponse:
172
+ """
173
+ Perform product search using the Search API.
174
+
175
+ Parameters:
176
+ - query: Search query string
177
+ - start: Starting position for pagination (0-based)
178
+ - size: Number of results to return per page
179
+ - filters: Optional list of FilterSpec objects for narrowing results
180
+ - sort_fields: Optional list of SortSpec objects for custom sorting
181
+
182
+ Returns:
183
+ - SearchResponse object containing search results and metadata
184
+
185
+ Raises:
186
+ - HTTPException: If search operation fails
187
+ """
188
+ logger.info(f"Searching for '{query}' on website {CLIENT_ID}")
189
+
190
+ # Generate scope dictionary from filters
191
+ scope = {}
192
+ if filters:
193
+ for filter_spec in filters:
194
+ field = filter_spec.field
195
+ value = filter_spec.value
196
+ operator = filter_spec.operator
197
+ # Handle different operator types
198
+ if operator == "eq":
199
+ # Direct mapping for equality filters
200
+ scope[field] = value
201
+ elif operator == "range":
202
+ # For range filters, create or update min/max values
203
+ if field not in scope:
204
+ scope[field] = {}
205
+ # Handle range as dictionary with min/max keys
206
+ if isinstance(value, dict):
207
+ if "min" in value:
208
+ scope[field]["min"] = value["min"]
209
+ if "max" in value:
210
+ scope[field]["max"] = value["max"]
211
+ # Handle individual gte/lte operators
212
+ elif operator == "gte":
213
+ scope[field]["min"] = value
214
+ elif operator == "lte":
215
+ scope[field]["max"] = value
216
+ logger.debug(f"Generated scope from filters: {scope}")
217
+
218
+ if sort_fields:
219
+ sort_fields_list = [{sort_field.field: {"order": sort_field.order, "type": sort_field.type}} for sort_field in sort_fields]
220
+ logger.info(f"Sort fields: {sort_fields_list}")
221
+ else:
222
+ sort_fields_list = None
223
+
224
+ # Build search request based on sample-body.json format
225
+ search_request = {
226
+ "q": query,
227
+ "website_id": str(CLIENT_ID).lower(),
228
+ "client": CLIENT_SHORTCODE,
229
+ "size": size,
230
+ "start": start,
231
+ "scope": scope
232
+ }
233
+ if sort_fields_list:
234
+ search_request["sort_fields"] = sort_fields_list
235
+
236
+ # Call Search API
237
+ start_time = time.time()
238
+ max_retries = 3
239
+ for attempt in range(max_retries):
240
+ try:
241
+ token = await get_auth_token(client_id=CLIENT_ID)
242
+ logger.info(f"Sending search request (attempt {attempt+1}): {json.dumps(search_request)[:500]}...")
243
+ response = requests.post(
244
+ SEARCH_API_ENDPOINT,
245
+ json=search_request,
246
+ headers={
247
+ "Authorization": f"Bearer {token}",
248
+ "Content-Type": "application/json"
249
+ },
250
+ timeout=15
251
+ )
252
+ if response.status_code == 401:
253
+ logger.warning(f"Received 401 Unauthorized from search API (attempt {attempt+1}). Invalidating token cache and retrying...")
254
+ # Invalidate token cache for this client
255
+ if CLIENT_ID in token_cache:
256
+ del token_cache[CLIENT_ID]
257
+ if attempt < max_retries - 1:
258
+ continue # Try again with a new token
259
+ else:
260
+ response.raise_for_status() # Will raise HTTPError
261
+ response.raise_for_status()
262
+ data = response.json()
263
+ payload = data.get("payload")
264
+ logger.info(f"Search API response: {payload}")
265
+ execution_time_ms = int((time.time() - start_time) * 1000)
266
+ total_results = payload.get("total_results", 0)
267
+ page_size = size
268
+ total_pages = max(1, (total_results + page_size - 1) // page_size)
269
+ current_page = start // page_size + 1
270
+ pagination = PaginationInfo(
271
+ current_page=current_page,
272
+ total_pages=total_pages,
273
+ total_results=total_results,
274
+ page_size=page_size
275
+ )
276
+ return SearchResponse(
277
+ results=payload.get("results", []),
278
+ pagination=pagination,
279
+ aggregations=payload.get("aggregations"),
280
+ suggestions=payload.get("suggestions", {}).get("fuzzy_suggestions", []),
281
+ redirect_url=payload.get("redirect_url"),
282
+ execution_time_ms=execution_time_ms
283
+ )
284
+ except requests.HTTPError as e:
285
+ if hasattr(e.response, 'status_code') and e.response.status_code == 401:
286
+ logger.warning(f"HTTPError 401 on attempt {attempt+1}: {e}")
287
+ if CLIENT_ID in token_cache:
288
+ del token_cache[CLIENT_ID]
289
+ if attempt < max_retries - 1:
290
+ continue
291
+ logger.error(f"Search failed with HTTPError: {e}")
292
+ raise HTTPException(status_code=500, detail=f"Search operation failed: {e}")
293
+ except Exception as e:
294
+ logger.error(f"Search failed: {e}")
295
+ raise HTTPException(status_code=500, detail=f"Search operation failed: {e}")
296
+ # If we exit the loop, all retries failed
297
+ raise HTTPException(status_code=500, detail="Search operation failed after multiple retries due to authentication errors.")
298
+
299
+ # Create FastMCP server with SSE settings
300
+ mcp_server = FastMCP(
301
+ name="MCP Search Server",
302
+ host=HOST,
303
+ port=PORT,
304
+ message_path=MESSAGE_PATH,
305
+ debug=True,
306
+ log_level="INFO"
307
+ )
308
+
309
+ # Register MCP tools
310
+ @mcp_server.tool("search")
311
+ async def search(
312
+ query: str,
313
+ start: int = 0,
314
+ size: int = 20,
315
+ filters: List[FilterSpec] = None,
316
+ ctx: Context = None
317
+ ) -> SearchResponse:
318
+ """
319
+ Execute a product search with optional filters.
320
+
321
+ Parameters:
322
+ - query: The search query text (e.g., "blue shirts", "tires", "dress")
323
+ - start: Starting position for pagination (0-based index)
324
+ - size: Number of results to return per page
325
+ - filters: Optional list of filters to narrow search results
326
+ Format: [{"field": "color", "value": "blue", "operator": "eq"}]
327
+ Supported operators:
328
+ - "eq" (equals): {"field": "color", "value": "blue", "operator": "eq"}
329
+ - "range" (for min/max values): {"field": "price", "value": {"min": 20, "max": 100}, "operator": "range"}
330
+
331
+ Returns:
332
+ - SearchResponse object containing results, pagination info, and optional metadata
333
+
334
+ Authentication is handled automatically using environment variables.
335
+ """
336
+ # Log the search request
337
+ if ctx:
338
+ await ctx.info(f"Searching for '{query}' on website: {CLIENT_ID}")
339
+
340
+ return await perform_search(
341
+ query=query,
342
+ start=start,
343
+ size=size,
344
+ filters=filters
345
+ )
346
+
347
+ # Add specialized search tool
348
+ @mcp_server.tool("filtered_search")
349
+ async def filtered_search(
350
+ query: str,
351
+ filters: List[FilterSpec],
352
+ start: int = 0,
353
+ size: int = 20,
354
+ ctx: Context = None
355
+ ) -> SearchResponse:
356
+ """
357
+ Search products with specific filters.
358
+
359
+ Parameters:
360
+ - query: The search query text (can be empty if using filters only)
361
+ - filters: List of filters to narrow search results
362
+ Format: [{"field": "field_name", "value": value, "operator": "eq"}]
363
+ Examples:
364
+ - Category filter: {"field": "product_category", "value": "Clothing > Shirts", "operator": "eq"}
365
+ - Color filter: {"field": "color", "value": "blue", "operator": "eq"}
366
+ - Price range: {"field": "price", "value": {"min": 20, "max": 100}, "operator": "range"}
367
+ - start: Starting position for pagination (0-based index)
368
+ - size: Number of results to return per page
369
+
370
+ Returns:
371
+ - SearchResponse object containing results, pagination info, and optional metadata
372
+ """
373
+ if ctx:
374
+ await ctx.info(f"Filtered search for '{query}' with filters: {filters}")
375
+
376
+ return await perform_search(
377
+ query=query,
378
+ start=start,
379
+ size=size,
380
+ filters=filters
381
+ )
382
+
383
+ # Add sorted search tool
384
+ @mcp_server.tool("sorted_search")
385
+ async def sorted_search(
386
+ query: str,
387
+ sort: List[SortSpec],
388
+ start: int = 0,
389
+ size: int = 20,
390
+ filters: List[FilterSpec] = None,
391
+ ctx: Context = None
392
+ ) -> SearchResponse:
393
+ """
394
+ Search products with custom sorting options.
395
+
396
+ Parameters:
397
+ - query: The search query text (can be empty if using filters only)
398
+ - sort: List of sort specifications to order results
399
+ Format: [{"field": "field_name", "order": "asc|desc", "type": "number|text|date"}]
400
+ Examples:
401
+ - Price (lowest first): {"field": "price", "order": "asc", "type": "number"}
402
+ - Popularity (highest first): {"field": "popularity", "order": "desc", "type": "number"}
403
+ - Name (alphabetical): {"field": "title", "order": "asc", "type": "text"}
404
+ - start: Starting position for pagination (0-based index)
405
+ - size: Number of results to return per page
406
+ - filters: Optional list of filters to narrow search results
407
+ Same format as the 'filters' parameter in the search and filtered_search tools
408
+
409
+ Returns:
410
+ - SearchResponse object containing results, pagination info, and optional metadata
411
+ """
412
+ if ctx:
413
+ await ctx.info(f"Sorted search for '{query}' with sort: {sort}")
414
+
415
+ return await perform_search(
416
+ query=query,
417
+ start=start,
418
+ size=size,
419
+ filters=filters or [],
420
+ sort_fields=sort
421
+ )
422
+
423
+
424
+ # Register resources
425
+ @mcp_server.resource(
426
+ uri="resource://search/docs",
427
+ name="Search Documentation",
428
+ description="Documentation for the product search API",
429
+ mime_type="application/json",
430
+ )
431
+ async def search_docs_resource() -> str:
432
+ """
433
+ Provides comprehensive documentation about the search tools with parameter descriptions and examples.
434
+ This resource includes details on all available search tools, parameter formats, and usage examples.
435
+
436
+ Returns:
437
+ - JSON string containing search API documentation
438
+ """
439
+
440
+ docs = {
441
+ "name": "search",
442
+ "description": "Execute a product search against the Search API with optional filters",
443
+ "parameters": {
444
+ "query": {
445
+ "type": "string",
446
+ "description": "The search query string (e.g., 'blue shirts', 'tires')"
447
+ },
448
+ "start": {
449
+ "type": "integer",
450
+ "description": "Starting position for results (0-based)",
451
+ "default": 0
452
+ },
453
+ "size": {
454
+ "type": "integer",
455
+ "description": "Number of results per page",
456
+ "default": 20
457
+ },
458
+ "filters": {
459
+ "type": "array",
460
+ "description": "Optional filters to narrow search results",
461
+ "items": {
462
+ "type": "object",
463
+ "properties": {
464
+ "field": {
465
+ "type": "string",
466
+ "description": "Field name to filter on (e.g., 'color', 'price', 'product_category')"
467
+ },
468
+ "value": {
469
+ "type": ["string", "number", "object"],
470
+ "description": "Filter value - can be string/number for equality or object with min/max for ranges"
471
+ },
472
+ "operator": {
473
+ "type": "string",
474
+ "enum": ["eq", "range", "gte", "lte"],
475
+ "description": "Filter operator - 'eq' for equality, 'range' for min/max values",
476
+ "default": "eq"
477
+ }
478
+ },
479
+ "required": ["field", "value"]
480
+ }
481
+ }
482
+ },
483
+ "tools": {
484
+ "search": {
485
+ "description": "Basic search with optional filters",
486
+ "parameters": ["query", "start", "size", "filters"]
487
+ },
488
+ "filtered_search": {
489
+ "description": "Search with mandatory filters",
490
+ "parameters": ["query", "filters", "start", "size"]
491
+ },
492
+ "sorted_search": {
493
+ "description": "Search with custom sorting options",
494
+ "parameters": ["query", "sort", "start", "size", "filters"]
495
+ }
496
+ },
497
+ "sorting": {
498
+ "description": "Sorting specifications for sorted_search tool",
499
+ "sort_spec": {
500
+ "field": {
501
+ "type": "string",
502
+ "description": "Field name to sort by (e.g., 'price', 'popularity')"
503
+ },
504
+ "order": {
505
+ "type": "string",
506
+ "enum": ["asc", "desc"],
507
+ "description": "Sort order - 'asc' for ascending, 'desc' for descending",
508
+ "default": "desc"
509
+ },
510
+ "type": {
511
+ "type": "string",
512
+ "enum": ["number", "text", "date"],
513
+ "description": "Type of the field being sorted",
514
+ "default": "number"
515
+ }
516
+ },
517
+ "sort_examples": [
518
+ {
519
+ "description": "Sort by price (lowest first)",
520
+ "sort": {"field": "price", "order": "asc", "type": "number"}
521
+ },
522
+ {
523
+ "description": "Sort by popularity (highest first)",
524
+ "sort": {"field": "popularity", "order": "desc", "type": "number"}
525
+ },
526
+ {
527
+ "description": "Sort by name (alphabetical)",
528
+ "sort": {"field": "title", "order": "asc", "type": "text"}
529
+ }
530
+ ]
531
+ },
532
+ "internal_configuration": {
533
+ "website_id": CLIENT_ID,
534
+ "client_shortcode": CLIENT_SHORTCODE
535
+ },
536
+ "filter_examples": [
537
+ {
538
+ "description": "Filter by color (equality)",
539
+ "filter": {"field": "color", "value": "blue", "operator": "eq"}
540
+ },
541
+ {
542
+ "description": "Filter by category (equality)",
543
+ "filter": {"field": "product_category", "value": "Clothing > Shirts", "operator": "eq"}
544
+ },
545
+ {
546
+ "description": "Filter by price range",
547
+ "filter": {"field": "price", "value": {"min": 20, "max": 100}, "operator": "range"}
548
+ }
549
+ ],
550
+ "search_examples": [
551
+ {
552
+ "description": "Basic search",
553
+ "params": {
554
+ "query": "dress",
555
+ "start": 0,
556
+ "size": 20
557
+ }
558
+ },
559
+ {
560
+ "description": "Search for tyres with pagination",
561
+ "params": {
562
+ "query": "tyres tires wheels",
563
+ "start": 10,
564
+ "size": 15
565
+ }
566
+ },
567
+ {
568
+ "description": "Search with color filter",
569
+ "params": {
570
+ "query": "shirts",
571
+ "filters": [
572
+ {"field": "color", "value": "blue", "operator": "eq"}
573
+ ]
574
+ }
575
+ },
576
+ {
577
+ "description": "Category search with price range",
578
+ "params": {
579
+ "query": "",
580
+ "filters": [
581
+ {"field": "product_category", "value": "Clothing > Shirts", "operator": "eq"},
582
+ {"field": "price", "value": {"min": 20, "max": 100}, "operator": "range"}
583
+ ]
584
+ }
585
+ }
586
+ ]
587
+ }
588
+ return json.dumps(docs, indent=2)
589
+
590
+ @mcp_server.resource(
591
+ uri="resource://search/response-schema",
592
+ name="Search Response Schema",
593
+ description="Schema describing the format of search response",
594
+ mime_type="application/json",
595
+ )
596
+ async def search_response_schema_resource() -> str:
597
+ """
598
+ Provides the JSON schema for search response objects.
599
+ This resource documents the structure of responses returned by all search tools.
600
+
601
+ Returns:
602
+ - JSON string containing the search response schema
603
+ """
604
+
605
+ schema = {
606
+ "type": "object",
607
+ "properties": {
608
+ "results": {
609
+ "type": "array",
610
+ "description": "List of product results",
611
+ "items": {
612
+ "type": "object",
613
+ "properties": {
614
+ "description": "Product properties vary based on search results with no enforced schema"
615
+ }
616
+ }
617
+ },
618
+ "pagination": {
619
+ "type": "object",
620
+ "properties": {
621
+ "current_page": {"type": "integer", "description": "Current page number"},
622
+ "total_pages": {"type": "integer", "description": "Total number of pages"},
623
+ "total_results": {"type": "integer", "description": "Total number of results"},
624
+ "page_size": {"type": "integer", "description": "Number of results per page"}
625
+ }
626
+ },
627
+ "aggregations": {
628
+ "type": "object",
629
+ "description": "Aggregation information for filtering"
630
+ },
631
+ "suggestions": {
632
+ "type": "array",
633
+ "description": "Search suggestions if query has few results",
634
+ "items": {
635
+ "type": "string"
636
+ }
637
+ },
638
+ "redirect_url": {
639
+ "type": "object",
640
+ "description": "URL information for redirects on specific queries"
641
+ },
642
+ "execution_time_ms": {
643
+ "type": "integer",
644
+ "description": "Time taken to execute the search in milliseconds"
645
+ }
646
+ }
647
+ }
648
+
649
+ return json.dumps(schema, indent=2)
650
+
651
+ @mcp_server.resource(
652
+ uri="resource://search/examples",
653
+ name="Search Examples",
654
+ description="Examples of different search patterns",
655
+ mime_type="application/json",
656
+ )
657
+ async def search_examples_resource() -> str:
658
+ """
659
+ Provides ready-to-use example search requests for common use cases.
660
+ This resource includes complete examples for all search tools with various parameter combinations.
661
+
662
+ Returns:
663
+ - JSON string containing practical search examples
664
+ """
665
+ examples = {
666
+ "basic_search": {
667
+ "description": "Basic product search",
668
+ "tool": "search",
669
+ "parameters": {
670
+ "query": "blue shirt",
671
+ "start": 0,
672
+ "size": 20
673
+ }
674
+ },
675
+ "filtered_category_search": {
676
+ "description": "Search for products in a specific category",
677
+ "tool": "filtered_search",
678
+ "parameters": {
679
+ "query": "",
680
+ "filters": [
681
+ {"field": "product_category", "value": "Clothing > Shirts", "operator": "eq"}
682
+ ],
683
+ "start": 0,
684
+ "size": 20
685
+ }
686
+ },
687
+ "price_range_search": {
688
+ "description": "Search for products within a price range",
689
+ "tool": "filtered_search",
690
+ "parameters": {
691
+ "query": "dress",
692
+ "filters": [
693
+ {"field": "price", "value": {"min": 29.99, "max": 99.99}, "operator": "range"}
694
+ ],
695
+ "start": 0,
696
+ "size": 20
697
+ }
698
+ },
699
+ "tyres_search": {
700
+ "description": "Search for tyres/tires with multiple filters",
701
+ "tool": "filtered_search",
702
+ "parameters": {
703
+ "query": "tyres tires wheels",
704
+ "filters": [
705
+ {"field": "product_category", "value": "Automotive > Tires", "operator": "eq"},
706
+ {"field": "size", "value": "205/55R16", "operator": "eq"}
707
+ ],
708
+ "start": 0,
709
+ "size": 15
710
+ }
711
+ },
712
+ "sorted_price_search": {
713
+ "description": "Search for products sorted by price (lowest first)",
714
+ "tool": "sorted_search",
715
+ "parameters": {
716
+ "query": "laptop",
717
+ "sort": [
718
+ {"field": "price", "order": "asc", "type": "number"}
719
+ ],
720
+ "start": 0,
721
+ "size": 20
722
+ }
723
+ },
724
+ "sorted_filtered_search": {
725
+ "description": "Search with both filters and custom sorting",
726
+ "tool": "sorted_search",
727
+ "parameters": {
728
+ "query": "sneakers",
729
+ "sort": [
730
+ {"field": "popularity", "order": "desc", "type": "number"}
731
+ ],
732
+ "filters": [
733
+ {"field": "brand", "value": "Nike", "operator": "eq"},
734
+ {"field": "price", "value": {"min": 50, "max": 150}, "operator": "range"}
735
+ ],
736
+ "start": 0,
737
+ "size": 20
738
+ }
739
+ },
740
+ "multiple_sort_fields": {
741
+ "description": "Search with multiple sort fields (primary and secondary ordering)",
742
+ "tool": "sorted_search",
743
+ "parameters": {
744
+ "query": "shoes",
745
+ "sort": [
746
+ {"field": "price", "order": "asc", "type": "number"},
747
+ {"field": "rating", "order": "desc", "type": "number"}
748
+ ],
749
+ "start": 0,
750
+ "size": 20
751
+ }
752
+ }
753
+ }
754
+ return json.dumps(examples, indent=2)
755
+
756
+ # Add a simple lifespan function to handle server startup
757
+ @asynccontextmanager
758
+ async def server_lifespan(app: FastMCP):
759
+ """
760
+ Lifespan handler for the FastMCP server to manage startup and shutdown operations.
761
+
762
+ On startup:
763
+ - Verifies the authentication service is working
764
+ - Pre-fetches a token for faster initial requests
765
+
766
+ Parameters:
767
+ - app: The FastMCP server instance
768
+ """
769
+ logger.info("Server starting up")
770
+
771
+ # Pre-fetch a token to verify auth service is working
772
+ try:
773
+ await get_auth_token()
774
+ logger.info("Auth service verified successfully")
775
+ except Exception as e:
776
+ logger.error(f"Auth service check failed: {e}")
777
+
778
+ yield # Server runs
779
+
780
+ logger.info("Server shutting down")
781
+
782
+ # Assign lifespan
783
+ mcp_server.settings.lifespan = server_lifespan
784
+
785
+ # Main entry point: run via SSE
786
+ if __name__ == "__main__":
787
+ logger.info(f"Starting MCP Search Server on http://{HOST}:{PORT}")
788
+ mcp_server.run()
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "iflow-mcp_particular-audience-mcp-search-server"
3
+ version = "0.1.0"
4
+ description = "MCP Search Server"
5
+ requires-python = ">=3.11"
6
+
7
+ dependencies = [
8
+ "fastapi>=0.95.0",
9
+ "uvicorn>=0.21.0",
10
+ "requests>=2.28.0",
11
+ "pydantic>=1.10.7",
12
+ "mcp>=0.5.0",
13
+ "mcp-use>=1.2.0",
14
+ "fastembed>=0.4.2",
15
+ "python-dotenv>=1.0.0",
16
+ "gql>=3.0.0",
17
+ "requests-toolbelt>=1.0.0"
18
+ ]
19
+
20
+ [scripts]
21
+ server = "mcp_search_server:main"
22
+
23
+ [tool.setuptools]
24
+ py-modules = ["mcp_search_server"]
25
+
26
+ [tool.setuptools.packages.find]
27
+ include = ["mcp_search_server"]
28
+ exclude = ["sample_usage.py"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+