-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
468 lines (402 loc) · 17.1 KB
/
Copy pathmain.py
File metadata and controls
468 lines (402 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
"""
Algolia MCP Server - Production Ready
AI Tinkerers Hackathon - DeepAgents Integration
Robust error handling and validation
"""
from mcp.server.fastmcp import FastMCP
from pydantic import Field
import mcp.types as types
from algoliasearch.search.client import SearchClient
import os
import time
import json
import hashlib
from typing import Dict, Any, List, Optional
from dotenv import load_dotenv
load_dotenv()
# Initialize MCP server
mcp = FastMCP(
"Algolia Vector Search Server",
port=3000,
stateless_http=True,
debug=True,
)
def get_client() -> Optional[SearchClient]:
"""Create Algolia client with proper validation."""
app_id = os.getenv("ALGOLIA_APP_ID")
api_key = os.getenv("ALGOLIA_API_KEY")
if not app_id or not api_key:
return None
return SearchClient(app_id, api_key)
def validate_client() -> Dict[str, Any]:
"""Validate client configuration."""
client = get_client()
if not client:
return {
"success": False,
"error": "Missing ALGOLIA_APP_ID or ALGOLIA_API_KEY environment variables"
}
return {"success": True, "client": client}
@mcp.tool(
title="Save Object",
description="Add a single record to an Algolia index with automatic task completion waiting. This tool ensures data consistency by waiting for the indexing operation to complete before returning. Perfect for adding individual documents, products, or any structured data to make it searchable. Automatically assigns objectID if not provided in the data.",
)
async def save_object(
index_name: str = Field(description="Algolia index name"),
object_id: str = Field(description="Unique object ID"),
object_data: str = Field(description="JSON string of object data"),
) -> Dict[str, Any]:
"""Save a single object to Algolia index."""
start_time = time.time()
# Validate client
validation = validate_client()
if not validation["success"]:
return {
**validation,
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
client = validation["client"]
try:
# Parse object data
try:
data = json.loads(object_data)
except json.JSONDecodeError:
return {
"success": False,
"error": "Invalid JSON in object_data",
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
# Ensure objectID is set
data["objectID"] = object_id
# Save object
save_resp = await client.save_object(
index_name=index_name,
body=data
)
# Wait for task completion
# save_resp is a SaveObjectResponse object with task_id attribute
await client.wait_for_task(
index_name=index_name,
task_id=save_resp.task_id
)
latency = (time.time() - start_time) * 1000
return {
"success": True,
"object_id": object_id,
"task_id": save_resp.task_id,
"index_name": index_name,
"performance": {
"latency_ms": latency,
"timestamp": time.time()
}
}
except Exception as e:
return {
"success": False,
"error": f"Error: {str(e)}",
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
@mcp.tool(
title="Save Objects Batch",
description="Efficiently add multiple records to an Algolia index in a single batch operation with automatic task completion waiting. Ideal for bulk data imports, content migrations, or periodic data updates. Automatically generates objectIDs for objects that don't have them. Provides performance metrics including objects per second throughput. Much faster than individual saves for large datasets.",
)
async def save_objects_batch(
index_name: str = Field(description="Algolia index name"),
objects_json: str = Field(description="JSON array of objects to save"),
) -> Dict[str, Any]:
"""Save multiple objects to Algolia index in batch."""
start_time = time.time()
# Validate client
validation = validate_client()
if not validation["success"]:
return {
**validation,
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
client = validation["client"]
try:
# Parse objects data
try:
objects = json.loads(objects_json)
if not isinstance(objects, list):
raise ValueError("Expected array of objects")
except (json.JSONDecodeError, ValueError) as e:
return {
"success": False,
"error": f"Invalid JSON array in objects_json: {str(e)}",
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
# Ensure all objects have objectID
for i, obj in enumerate(objects):
if "objectID" not in obj:
obj["objectID"] = f"auto_id_{i}_{int(time.time())}"
# Save objects in batch
# save_objects returns a list of BatchResponse objects
save_resps = await client.save_objects(
index_name=index_name,
objects=objects
)
# Wait for task completion - use the first response's task_id
if save_resps and len(save_resps) > 0:
task_id = save_resps[0].task_id
await client.wait_for_task(
index_name=index_name,
task_id=task_id
)
else:
task_id = None
latency = (time.time() - start_time) * 1000
return {
"success": True,
"objects_count": len(objects),
"task_id": task_id,
"index_name": index_name,
"sample_objects": objects[:3] if objects else [],
"performance": {
"latency_ms": latency,
"timestamp": time.time(),
"objects_per_second": len(objects) / (latency / 1000) if latency > 0 else 0
}
}
except Exception as e:
return {
"success": False,
"error": f"Error: {str(e)}",
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
@mcp.tool(
title="Search Index",
description="Perform advanced search queries on Algolia indexes with built-in typo tolerance, faceting, and highlighting capabilities. Supports complex filtering, attribute selection, and result customization. Returns comprehensive search analytics including processing time and hit counts. Perfect for implementing search functionality, content discovery, or data exploration with natural language queries.",
)
async def search_index(
index_name: str = Field(description="Algolia index name"),
query: str = Field(description="Search query"),
hits_per_page: int = Field(description="Number of results per page", default=20),
attributes_to_retrieve: str = Field(description="Comma-separated attributes to retrieve", default="*"),
attributes_to_highlight: str = Field(description="Comma-separated attributes to highlight", default=""),
filters: str = Field(description="Algolia filter expression", default=""),
) -> Dict[str, Any]:
"""Search Algolia index with comprehensive options."""
start_time = time.time()
# Validate client
validation = validate_client()
if not validation["success"]:
return {
**validation,
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
client = validation["client"]
try:
# Build search parameters
search_params = {
"indexName": index_name,
"query": query,
"hitsPerPage": hits_per_page
}
# Add optional parameters
if attributes_to_retrieve and attributes_to_retrieve != "*":
search_params["attributesToRetrieve"] = [
attr.strip() for attr in attributes_to_retrieve.split(",")
]
if attributes_to_highlight:
search_params["attributesToHighlight"] = [
attr.strip() for attr in attributes_to_highlight.split(",")
]
if filters:
search_params["filters"] = filters
# Perform search
results = await client.search({
"requests": [search_params]
})
latency = (time.time() - start_time) * 1000
# Extract results from response - results is a SearchResponses object
# results.results is a list of SearchResult objects
# Each SearchResult has an actual_instance which is a SearchResponse
if results.results and len(results.results) > 0:
search_result = results.results[0].actual_instance
# SearchResponse has attributes, not dict keys
hits = search_result.hits if hasattr(search_result, 'hits') else []
nb_hits = search_result.nb_hits if hasattr(search_result, 'nb_hits') else 0
processing_time = search_result.processing_time_ms if hasattr(search_result, 'processing_time_ms') else 0
else:
hits = []
nb_hits = 0
processing_time = 0
return {
"success": True,
"query": query,
"hits": [hit.model_dump() if hasattr(hit, 'model_dump') else hit for hit in hits],
"nb_hits": nb_hits,
"processing_time_ms": processing_time,
"index_name": index_name,
"search_params": search_params,
"performance": {
"latency_ms": latency,
"timestamp": time.time()
}
}
except Exception as e:
return {
"success": False,
"error": f"Error: {str(e)}",
"query": query,
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
@mcp.tool(
title="Index From Search Results",
description="Transform and index external search results (from SerpApi, Google Search, or other APIs) into Algolia for enhanced searchability and analytics. Automatically converts search result formats into Algolia-optimized objects with proper metadata, deduplication via URL hashing, and timestamp tracking. Perfect for creating searchable knowledge bases from web research, competitive analysis, or content aggregation workflows.",
)
async def index_from_search_results(
search_results_json: str = Field(description="JSON string of search results"),
index_name: str = Field(description="Algolia index name", default="realtime_search"),
) -> Dict[str, Any]:
"""Convert and index search results from external sources."""
start_time = time.time()
# Validate client
validation = validate_client()
if not validation["success"]:
return {
**validation,
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
client = validation["client"]
try:
# Parse search results
try:
search_data = json.loads(search_results_json)
except json.JSONDecodeError:
return {
"success": False,
"error": "Invalid JSON in search_results_json",
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
# Convert search results to Algolia objects
objects = []
for result in search_data.get("organic_results", []):
if not result.get("link"):
continue
object_id = hashlib.md5(result["link"].encode()).hexdigest()
algolia_object = {
"objectID": object_id,
"title": result.get("title", ""),
"snippet": result.get("snippet", ""),
"url": result.get("link", ""),
"position": result.get("position", 0),
"source_query": search_data.get("query", ""),
"indexed_at": time.time(),
"source": "external_search"
}
objects.append(algolia_object)
if not objects:
return {
"success": False,
"error": "No valid objects to index",
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
# Save objects
# save_objects returns a list of BatchResponse objects
save_resps = await client.save_objects(
index_name=index_name,
objects=objects
)
# Wait for completion - use the first response's task_id
if save_resps and len(save_resps) > 0:
task_id = save_resps[0].task_id
await client.wait_for_task(
index_name=index_name,
task_id=task_id
)
else:
task_id = None
latency = (time.time() - start_time) * 1000
return {
"success": True,
"indexed_count": len(objects),
"task_id": task_id,
"index_name": index_name,
"sample_objects": objects[:2],
"performance": {
"latency_ms": latency,
"timestamp": time.time()
}
}
except Exception as e:
return {
"success": False,
"error": f"Error: {str(e)}",
"performance": {"latency_ms": (time.time() - start_time) * 1000}
}
@mcp.resource(
uri="algolia://test/{index_name}",
description="Test Algolia connection by adding and searching a test record",
name="Connection Test",
)
async def test_connection(index_name: str) -> str:
"""Test Algolia connection following hello_algolia.py pattern."""
# Test record
test_record = {"objectID": "test-record-1", "name": "test record"}
# Add record
add_result = await save_object(
index_name=index_name,
object_id="test-record-1",
object_data=json.dumps({"name": "test record"})
)
if not add_result.get("success"):
return f"❌ Add failed: {add_result.get('error')}"
# Search for test
search_result = await search_index(
index_name=index_name,
query="test"
)
if not search_result.get("success"):
return f"❌ Search failed: {search_result.get('error')}"
return f"✅ Test passed - Added and found {search_result.get('nb_hits', 0)} results"
@mcp.resource(
uri="algolia://search/{index_name}/{query}",
description="Search results formatted for human reading",
name="Search Results",
)
async def search_results_formatted(index_name: str, query: str) -> str:
"""Get formatted search results."""
result = await search_index(index_name=index_name, query=query, hits_per_page=5)
if not result.get("success"):
return f"❌ Search error: {result.get('error')}"
lines = [f"Search results for '{query}' in {index_name}:"]
for i, hit in enumerate(result.get("hits", []), 1):
title = hit.get("title", hit.get("name", "No title"))
lines.append(f"{i}. {title}")
if "snippet" in hit:
lines.append(f" {hit['snippet']}")
if "url" in hit:
lines.append(f" {hit['url']}")
lines.append(f"\nTotal: {result.get('nb_hits', 0)} results ({result.get('processing_time_ms', 0)}ms)")
return "\n".join(lines)
@mcp.prompt(
name="algolia_workflow_prompt",
description="Generate workflow prompts for Algolia operations"
)
def algolia_workflow_prompt(
task: str = Field(description="Task: index, search, or test", default="search"),
data_source: str = Field(description="Data source description", default="search_results"),
) -> str:
"""Generate Algolia workflow prompts."""
if task == "index":
return f"Index {data_source} into Algolia: 1) Parse and validate data, 2) Create objects with proper objectIDs, 3) Batch save and wait for completion, 4) Verify indexing success."
elif task == "test":
return "Test Algolia connection: 1) Add test record, 2) Wait for indexing, 3) Search for test record, 4) Verify results."
else: # search
return f"Search Algolia effectively: 1) Craft semantic query, 2) Set appropriate filters and attributes, 3) Analyze results for relevance, 4) Extract key insights."
if __name__ == "__main__":
# Validate environment
app_id = os.getenv("ALGOLIA_APP_ID")
api_key = os.getenv("ALGOLIA_API_KEY")
if not app_id or not api_key:
print("❌ Missing ALGOLIA_APP_ID or ALGOLIA_API_KEY environment variables")
print("Please set your Algolia credentials before running the server")
else:
print("✅ Algolia credentials found")
print("🔍 Starting Robust Algolia MCP Server on port 3000...")
print("Tools: save_object, save_objects_batch, search_index, index_from_search_results")
print("Resources: algolia://test/{index}, algolia://search/{index}/{query}")
print("Prompts: algolia_workflow_prompt")
mcp.run(transport="streamable-http")