# MCP Wrapper for A2A in Synqed

Minimal Context Protocol (MCP) wrapper around Google A2A using Synqed architecture.

## Overview

This implementation provides an MCP server that exposes remote A2A agent capabilities as MCP tools, enabling AI assistants (like Claude Desktop) to interact with specialized agents via standardized protocols.

### Architecture

```
MCP Client (Claude) → MCP Server → A2A Client → Message Router → A2A Agents
                                                                ├─ Salesforce
                                                                ├─ Zoom
                                                                └─ Content Creator
```

### Key Components

- **MCP Server** (`mcp/server.py`): FastMCP-based server exposing 3 tool endpoints
- **A2A Client** (`mcp/a2a/client.py`): Wrapper around Synqed's MessageRouter for sending A2A tasks
- **Tool Modules** (`mcp/tools/`): Individual tool implementations (salesforce, zoom, content_creator)
- **Demo Script** (`demo.py`): Complete runnable example with agent setup

## Structure

```
mcp/
├── __init__.py              # Package initialization
├── server.py                # MCP server with FastMCP
├── a2a/
│   ├── __init__.py
│   └── client.py           # A2A task client wrapper
└── tools/
    ├── __init__.py
    ├── salesforce.py       # Salesforce query tool
    ├── zoom.py             # Zoom meeting creation tool
    └── content_creator.py  # Content generation tool

demo.py                      # Example run script
```

## Tools

### 1. `salesforce_query_leads`

Query Salesforce leads using SOQL.

**Input:**
- `query` (string): SOQL query string

**Output:**
- `results` (array): Lead records
- `count` (integer): Number of results
- `status` (string): Query status

**Example:**
```python
result = await session.call_tool(
    "salesforce_query_leads",
    arguments={"query": "SELECT Id, Name FROM Lead WHERE Status='New'"}
)
```

### 2. `zoom_create_meeting`

Create a scheduled Zoom meeting.

**Input:**
- `topic` (string): Meeting topic/title
- `start_time` (string): ISO 8601 timestamp
- `duration` (integer, optional): Duration in minutes (default: 60)
- `agenda` (string, optional): Meeting agenda

**Output:**
- `join_url` (string): Meeting join URL
- `meeting_id` (string): Unique meeting ID
- `password` (string): Meeting password
- `status` (string): Creation status

**Example:**
```python
result = await session.call_tool(
    "zoom_create_meeting",
    arguments={
        "topic": "Team Sync",
        "start_time": "2025-11-23T10:00:00Z",
        "duration": 60
    }
)
```

### 3. `content_creator_generate`

Generate content based on a prompt.

**Input:**
- `prompt` (string): Content generation instructions
- `tone` (string, optional): Desired tone (default: "professional")
- `format` (string, optional): Output format (default: "markdown")
- `max_length` (integer, optional): Max characters (default: 1000)

**Output:**
- `content` (string): Generated content
- `word_count` (integer): Word count
- `status` (string): Generation status

**Example:**
```python
result = await session.call_tool(
    "content_creator_generate",
    arguments={
        "prompt": "Write about AI agents",
        "tone": "informative",
        "format": "markdown"
    }
)
```

## Installation

### Requirements

- Python 3.10+
- Synqed (already installed in this repo)
- FastMCP (Model Context Protocol implementation)

### Install Dependencies

```bash
cd synqed-python
pip install fastmcp
```

## Usage

### 1. Run Demo (Recommended)

The demo script creates agents, sets up the MCP server, and tests all tools:

```bash
# Run with stdio transport (for Claude Desktop)
python demo.py

# Run with SSE transport (for web clients)
python demo.py --transport sse --port 8080

# Skip tests and start server directly
python demo.py --no-test
```

### 2. Run MCP Server Standalone

```bash
# Stdio mode
python -m mcp.server --transport stdio

# SSE mode
python -m mcp.server --transport sse --host localhost --port 8080
```

### 3. Use in Code

```python
import asyncio
from synqed import MessageRouter, Agent, AgentRuntimeRegistry
from mcp.server import MCPServer
from mcp.a2a.client import A2AClient

# Create agents
salesforce_agent = Agent(name="salesforce", logic=salesforce_logic, role="tools")
AgentRuntimeRegistry.register("salesforce", salesforce_agent)

# Setup router
router = MessageRouter()
router.register_agent("salesforce", salesforce_agent)

# Create MCP server
server = MCPServer(host="localhost", port=8080)
server.set_router(router)

# Run server
server.run(transport="stdio")
```

### 4. Connect from Claude Desktop

Add to your Claude Desktop MCP config (`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "synqed-a2a": {
      "command": "python",
      "args": ["-m", "mcp.server", "--transport", "stdio"],
      "cwd": "/path/to/synqed-python"
    }
  }
}
```

## Design Principles

This implementation strictly follows the user's requirements:

1. **Zero Invention**: Uses only existing Synqed + A2A constructs
2. **Strict Contracts**: Maintains A2A message format and semantics
3. **No New Agent Types**: All agents are standard Synqed Agents
4. **Clean Separation**: MCP layer is purely a translation wrapper
5. **Production Grade**: Fully typed, logged, and error-handled

### What We DON'T Do

- ❌ Create "email agents" or custom agent types
- ❌ Invent new message formats or protocols
- ❌ Bypass Synqed's routing infrastructure
- ❌ Modify AgentId format or workspace semantics
- ❌ Add abstractions beyond MCP + Synqed + A2A

### What We DO

- ✅ Use AgentId.from_email_like() / from_uri()
- ✅ Route via MessageRouter and Workspace
- ✅ Send standard A2A task messages
- ✅ Return clean JSON-compatible responses
- ✅ Handle errors gracefully with proper logging

## Testing

Run the demo with tests:

```bash
python demo.py
```

This will:
1. Create 3 A2A agents (Salesforce, Zoom, ContentCreator)
2. Register them in AgentRuntimeRegistry
3. Setup MessageRouter
4. Test each tool by sending A2A tasks
5. Start MCP server

Expected output:
```
============================================================
testing mcp tools via a2a client
============================================================

1. testing salesforce query...
result: {
  "status": "success",
  "message_id": "msg-...",
  "agent": "agent://tools/salesforce"
}

2. testing zoom meeting creation...
result: {
  "status": "success",
  "message_id": "msg-...",
  "agent": "agent://tools/zoom"
}

3. testing content generation...
result: {
  "status": "success",
  "message_id": "msg-...",
  "agent": "agent://tools/content_creator"
}

============================================================
all tests completed successfully!
============================================================
```

## Implementation Notes

### A2A Task Format

Tasks are sent as JSON messages:

```json
{
  "task_type": "query_leads",
  "payload": {
    "query": "SELECT * FROM Lead"
  }
}
```

### Agent Logic Pattern

All agents follow this pattern:

```python
async def agent_logic(context: AgentLogicContext) -> dict:
    latest = context.latest_message
    if not latest:
        return context.send("USER", "[agent ready]")
    
    try:
        task = json.loads(latest.content)
        task_type = task.get("task_type")
        payload = task.get("payload", {})
        
        # Process task
        result = process_task(task_type, payload)
        
        # Return response
        return context.send("MCPServer", json.dumps(result))
    
    except Exception as e:
        return context.send("MCPServer", json.dumps({
            "status": "error",
            "error": str(e)
        }))
```

### MCP Tool Registration

Tools are registered with FastMCP using decorators:

```python
@mcp.tool(name="tool_name", description="...")
async def tool_handler(param1: str, param2: int = 10) -> str:
    result = await a2a_client.send_task(...)
    return json.dumps(result)
```

## Extension Guide

To add a new tool:

1. **Create tool module** (`mcp/tools/newtool.py`):
   ```python
   async def new_function(a2a_client: A2AClient, param: str) -> dict:
       agent = AgentId.from_email_like("newtool@tools")
       return await a2a_client.send_task(agent, "task_type", {"param": param})
   
   TOOL_SCHEMA = {
       "name": "newtool_function",
       "description": "...",
       "input_schema": {...},
       "output_schema": {...}
   }
   ```

2. **Create agent logic** (in demo.py or separate module):
   ```python
   async def newtool_agent_logic(context: AgentLogicContext) -> dict:
       # Handle task and return response
       pass
   ```

3. **Register in MCP server** (`mcp/server.py`):
   ```python
   @self.mcp.tool(name="newtool_function", description=newtool.TOOL_SCHEMA["description"])
   async def newtool_function(param: str) -> str:
       result = await newtool.new_function(self.a2a_client, param)
       return json.dumps(result)
   ```

4. **Register agent** (in demo.py):
   ```python
   newtool_agent = Agent(name="newtool", logic=newtool_agent_logic, role="tools")
   AgentRuntimeRegistry.register("newtool", newtool_agent)
   ```

## Troubleshooting

### Server won't start
- Check Python version (3.10+ required)
- Ensure fastmcp is installed: `pip install fastmcp`
- Verify no port conflicts (for SSE mode)

### Tools not responding
- Check agents are registered: `AgentRuntimeRegistry._local_prototypes.keys()`
- Verify router has agents: `router._agents.keys()`
- Check logs for routing errors

### JSON parse errors
- Ensure task messages follow format: `{"task_type": "...", "payload": {...}}`
- Validate agent logic returns valid JSON strings
- Check for proper error handling in agent logic

## License

This code follows the same license as the Synqed project.

## Contributing

When contributing, ensure:
- No new abstractions beyond MCP + Synqed + A2A
- All code is fully typed
- Lowercase comments only
- Production-grade error handling
- Comprehensive logging

