# ARP Agent - LLM Agent for the ARP Platform ## An LLM-powered agent that connects to the ARP (Agent-native ERP) platform via the Model Context Protocol (MCP) and responds to Task and Message events in real-time. ## Table of Contents - [Overview](#overview) - [Installation](#installation) - [Configuration](#configuration) - [Running the Agent](#running-the-agent) - [How It Works](#how-it-works) - [MCP Communication](#mcp-communication) - [Available MCP Tools](#available-mcp-tools) - [Programmatic Usage](#programmatic-usage) - [Testing](#testing) --- ## Overview The ARP Agent connects to an ARP server and: 1. **Authenticates** via GraphQL login to obtain a JWT token 2. **Connects to MCP** via Server-Sent Events (SSE) to the `/mcp` endpoint 3. **Discovers Tools** using the MCP `tools/list` protocol 4. **Subscribes to Events** via MCP resources for real-time notifications 5. **Processes Events** using an LLM with tool-calling capabilities --- ## Installation ### Using pip ```bash pip install -r requirements.txt ``` ### Using Poetry (recommended) ```bash poetry install ``` ### Dependencies - `openai` - OpenAI API client for LLM interactions - `requests` - HTTP client for GraphQL and MCP communication - `sseclient-py` - Server-Sent Events client for MCP - `python-dotenv` - Environment variable management --- ## Configuration Copy the example environment file and configure your credentials: ```bash cp .env.example .env ``` Edit `.env` with your settings: ```env # ARP Server Configuration ARP_URL=http://localhost:8080 ARP_USERNAME=your-email@example.com ARP_PASSWORD=your-password # OpenAI Configuration OPENAI_API_KEY=sk-your-openai-api-key OPENAI_MODEL=gpt-4 OPENAI_TEMPERATURE=0.0 # Optional: Custom OpenAI endpoint (for local models, etc.) # OPENAI_BASE_URL=http://localhost:11434/v1 ``` ### Required Environment Variables | Variable | Description | |----------|-------------| | `ARP_URL` | Base URL of the ARP server | | `ARP_USERNAME` | Your ARP login email | | `ARP_PASSWORD` | Your ARP password | | `OPENAI_API_KEY` | OpenAI API key for LLM | ### Optional Environment Variables | Variable | Description | Default | |----------|-------------|---------| | `OPENAI_MODEL` | Model to use | `gpt-4` | | `OPENAI_TEMPERATURE` | Sampling temperature | `0.0` | | `OPENAI_BASE_URL` | Custom OpenAI-compatible endpoint | OpenAI API | --- ## Running the Agent ```bash python run_arp_agent.py ``` ### Expected Output ``` Testing connectivity to OpenAI API (api.openai.com)... ✓ Successfully connected to OpenAI API (api.openai.com) Connecting to ARP server at http://localhost:8080... Successfully authenticated with ARP server Connecting to MCP server... Discovered 3 MCP tools: ['introspect', 'query', 'mutate'] Initializing LLM agent... Agent initialized successfully. Subscribing to ARP resources... Available resources: ['taskCreated', 'taskUpdated', 'taskDeleted', 'messageAdded'] Subscribed to: graphql://subscription/taskCreated Subscribed to: graphql://subscription/taskUpdated Subscribed to: graphql://subscription/taskDeleted Subscribed to: graphql://subscription/messageAdded Listening for events. Press Ctrl+C to stop. ``` --- ## How It Works ### Architecture ``` ┌─────────────────┐ GraphQL Login ┌─────────────────┐ │ ARP Agent │ ──────────────────────► │ ARP Server │ │ │ ◄────────────────────── │ │ │ ┌───────────┐ │ JWT Token │ │ │ │ LLM │ │ │ ┌───────────┐ │ │ └───────────┘ │ SSE Connect │ │ MCP │ │ │ │ │ ──────────────────────► │ │ Server │ │ │ ▼ │ /mcp endpoint │ └───────────┘ │ │ ┌───────────┐ │ │ │ │ │ │MCP Client │◄─┼──── Tool Discovery │ │ │ │ └───────────┘ │ Tool Calls │ ▼ │ │ │ │ Notifications │ ┌───────────┐ │ │ ▼ │ │ │ GraphQL │ │ │ ┌───────────┐ │ │ │ Engine │ │ │ │ Events │◄─┼─────────────────────────┼──┤ │ │ │ └───────────┘ │ Real-time │ └───────────┘ │ └─────────────────┘ Subscriptions └─────────────────┘ ``` ### Event Processing Flow 1. **Event Received**: Task or message event via MCP resource notification 2. **Context Built**: Extract relevant details (ID, title, content, sender, etc.) 3. **LLM Invoked**: Agent processes the event with available tools 4. **Tool Execution**: LLM may call MCP tools to query/mutate data 5. **Response Generated**: Agent produces a result or takes action --- ## MCP Communication The agent uses the **Model Context Protocol (MCP)** to communicate with the ARP server: ### Connection Flow 1. **SSE Connection**: Connect to `/mcp` endpoint via Server-Sent Events 2. **Endpoint Discovery**: Receive message endpoint URL from `endpoint` event 3. **Initialize**: Send `initialize` request with protocol version and client info 4. **Tool Discovery**: Call `tools/list` to discover available tools 5. **Subscribe**: Call `resources/subscribe` for real-time event streams ### Authentication Authentication is handled via JWT tokens: 1. Login via GraphQL `login` mutation with email/password 2. Receive JWT token in response 3. Include token in SSE connection headers (`Authorization: Bearer `) 4. Token is automatically propagated to all MCP requests ### Protocol Details - **Protocol Version**: `2024-11-05` - **Transport**: HTTP POST for requests, SSE for responses/notifications - **Format**: JSON-RPC 2.0 --- ## Available MCP Tools The ARP MCP server exposes three tools: ### 1. `introspect` Discover the GraphQL schema - types, fields, queries, mutations. ```python # Get full schema result = mcp_client.call_tool("introspect", {}) # Get specific type result = mcp_client.call_tool("introspect", {"typeName": "User"}) ``` ### 2. `query` Execute GraphQL queries (read operations). ```python # Query users result = mcp_client.call_tool("query", { "query": "{ users { id email roles { name } } }" }) # Query with variables result = mcp_client.call_tool("query", { "query": "query User($id: ID!) { user(id: $id) { id email } }", "variables": {"id": "1"} }) ``` ### 3. `mutate` Execute GraphQL mutations (create/update/delete operations). ```python # Create a task result = mcp_client.call_tool("mutate", { "mutation": """ mutation CreateTask($input: NewTask!) { createTask(input: $input) { id title } } """, "variables": { "input": { "title": "New Task", "content": "Task description", "createdById": "1" } } }) # Delete a note result = mcp_client.call_tool("mutate", { "mutation": "mutation DeleteNote($id: ID!) { deleteNote(id: $id) }", "variables": {"id": "123"} }) ``` --- ## Programmatic Usage ### Using MCPClient Directly ```python from llm_agents.mcp_client import login_and_create_mcp_client # Login and create client client = login_and_create_mcp_client( url="http://localhost:8080", username="admin@example.com", password="secret123" ) # Connect to MCP server client.connect() client.initialize() # Discover tools tools = client.list_tools() print(f"Available tools: {[t.name for t in tools]}") # Call tools users = client.call_tool("query", {"query": "{ users { id email } }"}) print(users) # Subscribe to resources client.subscribe_resource("graphql://subscription/taskCreated") # Listen for notifications def on_notification(data): print(f"Received: {data}") client.listen_for_notifications(on_notification) # Cleanup client.close() ``` ### Using the Agent with MCP ```python from llm_agents import Agent, ChatLLM from llm_agents.mcp_client import login_and_create_mcp_client # Create authenticated MCP client mcp_client = login_and_create_mcp_client( url="http://localhost:8080", username="admin@example.com", password="secret123" ) # Connect and initialize mcp_client.connect() mcp_client.initialize() mcp_client.list_tools() # Create agent with MCP client llm = ChatLLM() # Uses OPENAI_API_KEY from environment agent = Agent(llm=llm, mcp_client=mcp_client) # Run the agent result = agent.run("List all users and their roles") print(result) # Cleanup mcp_client.close() ``` --- ## MCP Resources (Subscriptions) The ARP MCP server exposes resources for real-time GraphQL subscriptions: | Resource URI | Description | |--------------|-------------| | `graphql://subscription/taskCreated` | New task events (received by assignee) | | `graphql://subscription/taskUpdated` | Task update events (received by assignee) | | `graphql://subscription/taskDeleted` | Task deletion events (received by assignee) | | `graphql://subscription/messageAdded` | New message events (received by receivers) | ### Subscribing to Resources ```python # List available resources resources = mcp_client.list_resources() for resource in resources: print(f"{resource['uri']}: {resource['name']}") # Subscribe to task events mcp_client.subscribe_resource("graphql://subscription/taskCreated") mcp_client.subscribe_resource("graphql://subscription/taskUpdated") # Unsubscribe mcp_client.unsubscribe_resource("graphql://subscription/taskCreated") ``` --- ## Event Filtering Events are filtered by the ARP server based on user context: - **Task Events**: Only received for tasks where the user is the **assignee** - **Message Events**: Only received for messages where the user is a **receiver** This ensures each user only receives relevant notifications. --- ## Testing Run the integration tests (requires a running ARP server): ```bash python -m pytest tests/integration/ -v --no-cov ``` ### Test Categories - **Login Tests**: Authentication flow - **Connection Tests**: MCP connection and initialization - **Tool Tests**: Introspect, query, and mutate operations - **Resource Tests**: Subscription functionality - **Error Handling Tests**: Error responses and edge cases --- ## Project Structure ``` arp_agent/ ├── llm_agents/ │ ├── __init__.py # Package exports │ ├── agent.py # Agent with tool-calling │ ├── llm.py # OpenAI LLM wrapper │ └── mcp_client.py # MCP client implementation ├── tests/ │ ├── conftest.py # Pytest fixtures │ ├── test_setup_validation.py │ ├── unit/ # Unit tests │ └── integration/ # Integration tests │ └── test_arp_agent_integration.py ├── specs/ │ └── schema.graphqls # GraphQL schema reference ├── run_arp_agent.py # Main entry point ├── run_tests.py # Test runner wrapper ├── pyproject.toml # Poetry configuration ├── poetry.lock # Locked dependencies ├── requirements.txt # Python dependencies (for pip) ├── .env.example # Environment template ├── CLIENT_GUIDE.md # ARP client implementation guide └── README.md # This file ``` --- ## License See [LICENSE](LICENSE) for details.