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.
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/LICENSE +21 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/PKG-INFO +17 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/README.md +252 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/iflow_mcp_particular_audience_mcp_search_server.egg-info/PKG-INFO +17 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/iflow_mcp_particular_audience_mcp_search_server.egg-info/SOURCES.txt +9 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/iflow_mcp_particular_audience_mcp_search_server.egg-info/dependency_links.txt +1 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/iflow_mcp_particular_audience_mcp_search_server.egg-info/requires.txt +10 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/iflow_mcp_particular_audience_mcp_search_server.egg-info/top_level.txt +1 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/mcp_search_server.py +788 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/pyproject.toml +28 -0
- iflow_mcp_particular_audience_mcp_search_server-0.1.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp_search_server
|
|
@@ -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"]
|