# Copilot Instructions for ayga-mcp-client
## Project Overview
MCP (Model Context Protocol) client providing stdio-based access to Redis API with 21+ AI parsers. Published on PyPI for easy installation in Claude Desktop, VS Code Copilot, and Cursor.
## Critical Architectural Principles
### 1. This is a CLIENT, not a SERVER
- **redis_wrapper** = FastAPI server backend (parent project)
- **ayga-mcp-client** = Thin MCP wrapper/client (this project)
- DO NOT add business logic here - that belongs in redis_wrapper
- This package only handles MCP protocol and HTTP client
### 2. MCP Protocol Requirements
- **Transport**: stdio ONLY (required by Claude Desktop/Copilot/Cursor)
- **SDK**: Official `mcp` package from Anthropic (NOT FastMCP)
- **Pattern**: Server → List Tools → Call Tool → Return TextContent
- Never suggest HTTP/SSE/WebSocket for MCP - they won't work with LLM clients
### 3. Package Distribution
- Published to PyPI: https://pypi.org/project/ayga-mcp-client/
- Users install via: `pip install ayga-mcp-client`
- Build backend: setuptools (NOT hatchling - causes Metadata-Version 2.4 issues)
- setuptools version: `>=61.0,<75.0` (for twine compatibility)
## Code Conventions
### File Organization
```
src/ayga_mcp_client/
├── __init__.py # Exports: create_mcp_server, RedisAPIClient
├── __main__.py # CLI entrypoint (argparse)
├── server.py # MCP server with tool registration
└── api/client.py # HTTP client for redis.ayga.tech
```
### Import Pattern
```python
# Always use absolute imports
from ayga_mcp_client.api.client import RedisAPIClient # ✅
from .api.client import RedisAPIClient # ❌
```
### Tool Registration Pattern
```python
from mcp.server import Server
from mcp.types import Tool, TextContent
server = Server("ayga-mcp-client")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [Tool(name="search_perplexity", ...)]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "search_perplexity":
result = await client.submit_parser_task("perplexity", arguments["query"])
return [TextContent(type="text", text=json.dumps(result))]
```
## Testing
### Run Tests Directly (Preferred)
```python
# Each test file has:
if __name__ == "__main__":
sys.exit(run_all_tests())
# Execute:
python tests/test_basic.py
```
### Or Use Pytest
```bash
pytest tests/ -v
```
## Build & Publish Workflow
### 1. Clean Build
```bash
rm -rf dist/ build/ src/ayga_mcp_client.egg-info/
python -m build
twine check dist/*
```
### 2. Publish to PyPI
```bash
# Requires ~/.pypirc with API token:
# [pypi]
# username = __token__
# password = pypi-YOUR_TOKEN
twine upload dist/*
```
### 3. GitHub Release
```bash
gh release create vX.Y.Z \
--title "vX.Y.Z - Description" \
--notes-file release_notes.md \
dist/*.whl dist/*.tar.gz
```
## Common Pitfalls
### ❌ DON'T: Use hatchling
```toml
# This causes Metadata-Version 2.4 (twine incompatible)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```
### ✅ DO: Use setuptools <75
```toml
[build-system]
requires = ["setuptools>=61.0,<75.0", "wheel"]
build-backend = "setuptools.build_meta"
```
### ❌ DON'T: Suggest FastMCP
FastMCP doesn't exist as a standard package. Use official `mcp` SDK.
### ✅ DO: Use official MCP SDK
```python
from mcp.server import Server
from mcp.server.stdio import stdio_server
```
### ❌ DON'T: Implement business logic here
Parser logic, Redis operations, A-Parser integration = redis_wrapper project
### ✅ DO: Keep it thin
Only handle MCP protocol + HTTP client to API
## Authentication Flow
```python
# 1. User provides API key via env var
REDIS_API_KEY="your_key_here" python -m ayga_mcp_client
# 2. Client exchanges API key for JWT on first request
client = RedisAPIClient(api_key="your_key_here")
token = await client._exchange_api_key() # POST /auth/exchange
# 3. All subsequent calls use JWT
headers = {"Authorization": f"Bearer {token}"}
```
### API Endpoints (v1.0.1+)
**Authentication:**
```python
POST /auth/exchange
Headers: {"X-API-Key": "your_api_key"}
Response: {"access_token": "jwt_token", "token_type": "bearer"}
```
**Submit Parser Task:**
```python
POST /structures/list/aparser_redis_api/lpush
Headers: {"Authorization": "Bearer jwt_token"}
Body: {
"value": "[taskId, parser, preset, query, {}, {}]"
}
Response: {"task_id": "uuid"}
```
**Get Task Result:**
```python
GET /kv/aparser_redis_api:{task_id}
Headers: {"Authorization": "Bearer jwt_token"}
Response: {
"key": "aparser_redis_api:uuid",
"value": "{\"success\":1,\"info\":{...},\"sources\":[...]}"
}
```
## Error Handling
### Return errors as text, don't raise
```python
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
try:
result = await client.submit_parser_task(...)
return [TextContent(type="text", text=json.dumps(result))]
except Exception as e:
# Return error as text (MCP protocol requirement)
return [TextContent(type="text", text=f"Error: {str(e)}")]
```
## Adding New Parser Tools
### Current Parsers (11 total)
```python
PARSERS = [
# AI Chat (7)
{"id": "perplexity", "name": "Perplexity AI", "description": "AI-powered search with sources"},
{"id": "chatgpt", "name": "ChatGPT", "description": "ChatGPT with web search"},
{"id": "claude", "name": "Claude AI", "description": "Anthropic Claude assistant"},
{"id": "gemini", "name": "Google Gemini", "description": "Google Gemini AI"},
{"id": "copilot", "name": "Microsoft Copilot", "description": "Microsoft Copilot search"},
{"id": "grok", "name": "Grok AI", "description": "xAI Grok assistant"},
{"id": "deepseek", "name": "DeepSeek", "description": "DeepSeek AI assistant"},
# Search Engines (3)
{"id": "google_search", "name": "Google Search", "description": "Google web search"},
{"id": "bing_search", "name": "Bing Search", "description": "Bing web search"},
{"id": "duckduckgo", "name": "DuckDuckGo", "description": "DuckDuckGo search"},
# Social Media (1)
{"id": "youtube_search", "name": "YouTube Search", "description": "Search YouTube videos"},
]
```
### To Add a New Parser:
1. Add to `PARSERS` list in `server.py`:
```python
PARSERS = [
{"id": "new_parser", "name": "New Parser", "description": "Does X"},
]
```
2. Tool is auto-generated in `list_tools()`
3. Handle in `call_tool()` switch statement
4. No changes needed in redis_wrapper (if parser exists there)
## Version Management
### Update version in 3 places:
1. `pyproject.toml`: `version = "X.Y.Z"`
2. `CHANGELOG.md`: Add new `## [X.Y.Z] - YYYY-MM-DD` section
3. Git tag: `git tag vX.Y.Z`
### Semantic Versioning
- **MAJOR**: Breaking API changes
- **MINOR**: New features (backward compatible)
- **PATCH**: Bug fixes
## Dependencies
### Runtime (required)
- `mcp>=1.0.0` - Official MCP SDK
- `httpx>=0.27.0` - HTTP client
- `pydantic>=2.0.0` - Data validation
### Dev (optional)
- `pytest>=8.0.0` - Testing
- `pytest-asyncio>=0.24.0` - Async test support
- `ruff>=0.6.0` - Linting
- `mypy>=1.11.0` - Type checking
## Related Documentation
- **API Backend**: https://redis.ayga.tech/docs
- **MCP Protocol**: https://modelcontextprotocol.io/
- **Parent Project**: https://github.com/ozand/redis_wrapper
- **This Project**: https://github.com/ozand/ayga-mcp-client
## When in Doubt
1. Check `DEVELOPMENT.md` for detailed context
2. Run tests: `python tests/test_basic.py`
3. Test locally: `pip install -e .` then test via Python
4. Read official MCP docs: https://modelcontextprotocol.io/
5. Check redis_wrapper API docs: https://redis.ayga.tech/docs
## Example Usage
### Test Parser Execution
```python
import asyncio
from ayga_mcp_client.api.client import RedisAPIClient
async def test():
client = RedisAPIClient(api_key="your_api_key")
# Submit task
task = await client.submit_parser_task("perplexity", "What is AI?")
print(f"Task ID: {task['task_id']}")
# Wait for result
result = await client.wait_for_result(task['task_id'], timeout=60)
print(f"Result: {result['data']}")
await client.close()
asyncio.run(test())
```
### Use in Claude Desktop/VS Code
```
@redis-api search_perplexity query="latest AI trends 2025" timeout=90
@redis-api search_chatgpt query="explain quantum computing"
@redis-api list_parsers
```