# ARP Server Client Implementation Guide This document provides the necessary information to implement a client for the ARP (Agent-native ERP) GraphQL server. ## Table of Contents 1. [Overview](#overview) 2. [Connection & Endpoint](#connection--endpoint) 3. [Authentication](#authentication) 4. [Authorization & Permissions](#authorization--permissions) 5. [GraphQL Operations](#graphql-operations) 6. [Error Handling](#error-handling) 7. [Subscriptions](#subscriptions) 8. [MCP Server](#mcp-server) --- ## Overview The ARP server exposes a **GraphQL API** for managing users, roles, permissions, services, tasks, notes, and messages. All operations except `login` require authentication via a JWT bearer token. --- ## Connection & Endpoint - **Protocol**: HTTP/HTTPS - **Endpoint**: `/query` (GraphQL endpoint) - **Content-Type**: `application/json` - **Request Method**: `POST` for queries and mutations ### Example Request Structure ```json { "query": "string (GraphQL query or mutation)", "variables": "object (optional variables)", "operationName": "string (optional operation name)" } ``` --- ## Authentication ### Login Flow 1. Call the `login` mutation with email and password 2. Receive a JWT token in the response 3. Include the token in subsequent requests via the `Authorization` header ### Login Mutation ```graphql mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) { token user { id email roles { id name permissions { id code description } } } } } ``` **Variables:** ```json { "email": "user@example.com", "password": "your-password" } ``` **Response:** ```json { "data": { "login": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": "1", "email": "user@example.com", "roles": [...] } } } } ``` ### Using the Token Include the token in all subsequent requests: ``` Authorization: Bearer ``` ### Token Details | Property | Value | |----------|-------| | Algorithm | HS256 | | Expiration | 10 years from issuance | | Claims | `user_id`, `email`, `roles`, `permissions` | --- ## Authorization & Permissions ### Permission-Based Access Control Many operations require specific permissions. The permission format is `resource:action`. ### Required Permissions by Operation | Operation | Required Permission | |-----------|---------------------| | `updateUser` | `user:update` | | `deleteUser` | `user:delete` | | `updateNote` | `note:update` | | `deleteNote` | `note:delete` | | `updateRole` | `role:update` | | `deleteRole` | `role:delete` | | `updatePermission` | `permission:update` | | `deletePermission` | `permission:delete` | | `updateService` | `service:update` | | `deleteService` | `service:delete` | | `updateTask` | `task:update` | | `deleteTask` | `task:delete` | | `updateTaskStatus` | `taskstatus:update` | | `deleteTaskStatus` | `taskstatus:delete` | | `updateMessage` | `message:update` | | `deleteMessage` | `message:delete` | ### Authorization Errors If authentication or authorization fails, the API returns an error: ```json { "errors": [ { "message": "unauthorized: authentication required" } ] } ``` Or for missing permissions: ```json { "errors": [ { "message": "unauthorized: missing user:update permission" } ] } ``` --- ## GraphQL Operations ### Queries (Read Operations) All queries require authentication. #### Users ```graphql # Get all users query Users { users { id email roles { id name } createdAt updatedAt } } # Get single user by ID query User($id: ID!) { user(id: $id) { id email roles { id name permissions { id code description } } createdAt updatedAt } } ``` #### Notes ```graphql # Get all notes query Notes { notes { id title content userId user { id email } serviceId service { id name } createdAt updatedAt } } # Get single note by ID query Note($id: ID!) { note(id: $id) { id title content userId serviceId createdAt updatedAt } } ``` #### Roles ```graphql # Get all roles query Roles { roles { id name description permissions { id code description } } } # Get single role by ID query Role($id: ID!) { role(id: $id) { id name description permissions { id code } } } ``` #### Permissions ```graphql # Get all permissions query Permissions { permissions { id code description } } # Get single permission by ID query Permission($id: ID!) { permission(id: $id) { id code description } } ``` #### Services ```graphql # Get all services query Services { services { id name description createdById createdBy { id email } participants { id email } tasks { id title } createdAt updatedAt } } # Get single service by ID query Service($id: ID!) { service(id: $id) { id name description participants { id email } tasks { id title status { code label } } createdAt updatedAt } } ``` #### Tasks ```graphql # Get all tasks query Tasks { tasks { id title content createdById createdBy { id email } updatedById updatedBy { id email } assigneeId assignee { id email } statusId status { id code label } dueDate priority createdAt updatedAt } } # Get single task by ID query Task($id: ID!) { task(id: $id) { id title content createdById createdBy { id email } updatedById updatedBy { id email } assignee { id email } status { code label } dueDate priority createdAt updatedAt } } ``` #### Task Statuses ```graphql # Get all task statuses query TaskStatuses { taskStatuses { id code label tasks { id title } createdAt updatedAt } } # Get single task status by ID query TaskStatus($id: ID!) { taskStatus(id: $id) { id code label tasks { id title } createdAt updatedAt } } ``` #### Messages ```graphql # Get all messages query Messages { messages { id senderId sender { id email } content sentAt receivers createdAt updatedAt } } # Get single message by ID query Message($id: ID!) { message(id: $id) { id senderId sender { id email } content sentAt receivers createdAt updatedAt } } ``` ### Mutations (Write Operations) All mutations require authentication. Some require additional permissions (see Authorization section). #### Authentication ```graphql mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) { token user { id email } } } ``` #### Users ```graphql # Create user mutation CreateUser($input: NewUser!) { createUser(input: $input) { id email roles { id name } createdAt } } # Variables: { "input": { "email": "...", "password": "...", "roles": ["1", "2"] } } # Update user (requires user:update permission) mutation UpdateUser($id: ID!, $input: UpdateUserInput!) { updateUser(id: $id, input: $input) { id email roles { id name } updatedAt } } # Variables: { "id": "1", "input": { "email": "new@example.com" } } # Delete user (requires user:delete permission) mutation DeleteUser($id: ID!) { deleteUser(id: $id) } ``` #### Notes ```graphql # Create note mutation CreateNote($input: NewNote!) { createNote(input: $input) { id title content userId serviceId createdAt } } # Variables: { "input": { "title": "...", "content": "...", "userId": "1", "serviceId": "1" } } # Update note (requires note:update permission) mutation UpdateNote($id: ID!, $input: UpdateNoteInput!) { updateNote(id: $id, input: $input) { id title content updatedAt } } # Delete note (requires note:delete permission) mutation DeleteNote($id: ID!) { deleteNote(id: $id) } ``` #### Roles ```graphql # Create role mutation CreateRole($input: NewRole!) { createRole(input: $input) { id name description permissions { id code } createdAt } } # Variables: { "input": { "name": "...", "description": "...", "permissions": ["1", "2"] } } # Update role (requires role:update permission) mutation UpdateRole($id: ID!, $input: UpdateRoleInput!) { updateRole(id: $id, input: $input) { id name permissions { id code } updatedAt } } # Delete role (requires role:delete permission) mutation DeleteRole($id: ID!) { deleteRole(id: $id) } ``` #### Permissions ```graphql # Create permission mutation CreatePermission($input: NewPermission!) { createPermission(input: $input) { id code description createdAt } } # Variables: { "input": { "code": "resource:action", "description": "..." } } # Update permission (requires permission:update permission) mutation UpdatePermission($id: ID!, $input: UpdatePermissionInput!) { updatePermission(id: $id, input: $input) { id code updatedAt } } # Delete permission (requires permission:delete permission) mutation DeletePermission($id: ID!) { deletePermission(id: $id) } ``` #### Services ```graphql # Create service mutation CreateService($input: NewService!) { createService(input: $input) { id name description createdById participants { id email } createdAt } } # Variables: { "input": { "name": "...", "description": "...", "createdById": "1", "participants": ["1", "2"] } } # Update service (requires service:update permission) mutation UpdateService($id: ID!, $input: UpdateServiceInput!) { updateService(id: $id, input: $input) { id name participants { id email } updatedAt } } # Delete service (requires service:delete permission) mutation DeleteService($id: ID!) { deleteService(id: $id) } ``` #### Tasks ```graphql # Create task mutation CreateTask($input: NewTask!) { createTask(input: $input) { id title content createdById assigneeId statusId dueDate priority createdAt } } # Variables: { "input": { "title": "...", "content": "...", "createdById": "1", "priority": "high" } } # Update task (requires task:update permission) # Note: updatedBy is automatically set to the current authenticated user mutation UpdateTask($id: ID!, $input: UpdateTaskInput!) { updateTask(id: $id, input: $input) { id title status { code label } assignee { id email } updatedBy { id email } dueDate priority updatedAt } } # Delete task (requires task:delete permission) mutation DeleteTask($id: ID!) { deleteTask(id: $id) } ``` #### Task Statuses ```graphql # Create task status mutation CreateTaskStatus($input: NewTaskStatus!) { createTaskStatus(input: $input) { id code label createdAt } } # Variables: { "input": { "code": "in_progress", "label": "In Progress" } } # Update task status (requires taskstatus:update permission) mutation UpdateTaskStatus($id: ID!, $input: UpdateTaskStatusInput!) { updateTaskStatus(id: $id, input: $input) { id code label updatedAt } } # Delete task status (requires taskstatus:delete permission) mutation DeleteTaskStatus($id: ID!) { deleteTaskStatus(id: $id) } ``` #### Messages ```graphql # Create message # Note: sender is automatically set to the authenticated user mutation CreateMessage($input: NewMessage!) { createMessage(input: $input) { id senderId sender { id email } content sentAt receivers createdAt } } # Variables: { "input": { "content": "Hello!", "receivers": ["2", "3"] } } # Update message (requires message:update permission) mutation UpdateMessage($id: ID!, $input: UpdateMessageInput!) { updateMessage(id: $id, input: $input) { id content receivers updatedAt } } # Delete message (requires message:delete permission) mutation DeleteMessage($id: ID!) { deleteMessage(id: $id) } ``` --- ## Error Handling ### Error Response Format Errors are returned in the standard GraphQL error format: ```json { "errors": [ { "message": "Error description", "path": ["fieldName"], "locations": [{ "line": 1, "column": 2 }] } ], "data": null } ``` ### Common Error Messages | Error Message | Cause | |---------------|-------| | `unauthorized: authentication required` | Missing or invalid JWT token | | `unauthorized: missing X:Y permission` | User lacks required permission | | `invalid credentials` | Wrong email/password on login | | `invalid X ID` | Malformed ID provided | | `X not found` | Resource with given ID doesn't exist | | `failed to create/update/delete X` | Database operation failed | --- ## Subscriptions The API supports real-time updates via GraphQL subscriptions. Subscriptions use WebSocket connections and provide filtered events based on user context. ### Event Filtering Subscriptions are **filtered by user context**. Users only receive events that are relevant to them: | Subscription | Filtering Rule | |--------------|----------------| | `taskCreated` | Only if user is the **assignee** | | `taskUpdated` | Only if user is the **assignee** | | `taskDeleted` | Only if user is the **assignee** | | `messageAdded` | Only if user is a **receiver** of the message | This means: - A user will only receive task events for tasks assigned to them - A user will only receive message events for messages where they are a receiver - Unassigned tasks do not trigger notifications to any user ### Available Subscriptions ```graphql # Task created - received only by assignee subscription TaskCreated { taskCreated { id title content assigneeId status { code label } priority createdAt } } # Task updated - received only by assignee subscription TaskUpdated { taskUpdated { id title content assigneeId updatedBy { id email } status { code label } priority updatedAt } } # Task deleted - received only by assignee subscription TaskDeleted { taskDeleted { id title assigneeId } } # Message added - received by message receivers subscription MessageAdded { messageAdded { id senderId sender { id email } content sentAt receivers } } ``` ### WebSocket Connection To use subscriptions, establish a WebSocket connection: 1. **Protocol**: WebSocket over HTTP/HTTPS 2. **Endpoint**: `/query` (same as GraphQL endpoint) 3. **Authentication**: Include the JWT token in the connection parameters or headers ### WebSocket Subprotocol The server uses the standard GraphQL over WebSocket protocol (`graphql-ws`): #### Connection Initialization ```json { "type": "connection_init", "payload": { "Authorization": "Bearer " } } ``` #### Subscribe to a Topic ```json { "id": "1", "type": "start", "payload": { "query": "subscription { taskCreated { id title assigneeId } }" } } ``` #### Receive Events ```json { "id": "1", "type": "data", "payload": { "data": { "taskCreated": { "id": "5", "title": "New Task", "assigneeId": "2" } } } } ``` #### Unsubscribe ```json { "id": "1", "type": "stop" } ``` ### Example: JavaScript/TypeScript Client Using the `graphql-ws` library: ```javascript import { createClient } from 'graphql-ws'; const client = createClient({ url: 'wss://api.example.com/query', connectionParams: { Authorization: `Bearer ${token}`, }, }); // Subscribe to taskCreated events const unsubscribe = client.subscribe( { query: `subscription { taskCreated { id title assigneeId } }`, }, { next: (data) => { console.log('Task created:', data.data.taskCreated); }, error: (error) => { console.error('Subscription error:', error); }, complete: () => { console.log('Subscription closed'); }, } ); // Later: unsubscribe unsubscribe(); ``` ### Example: Go Client Using `github.com/99designs/gqlgen/client`: ```go import ( "github.com/99designs/gqlgen/client" ) // Create WebSocket client with authentication wsClient := client.New(server, client.AddHeader("Authorization", "Bearer "+token)) // Subscribe to taskCreated subscription := wsClient.Websocket(`subscription { taskCreated { id title } }`) defer subscription.Close() for { var response struct { TaskCreated *model.Task `json:"taskCreated"` } err := subscription.Next(&response) if err != nil { break // Connection closed or error } if response.TaskCreated != nil { fmt.Printf("Task created: %s\n", response.TaskCreated.Title) } } ``` ### Subscription Use Cases | Use Case | Subscription | Notes | |----------|--------------|-------| | Task assignment notifications | `taskCreated` | User receives event when assigned a new task | | Task status updates | `taskUpdated` | User receives event when their assigned task is modified | | Task removal | `taskDeleted` | User receives event when their assigned task is deleted | | Direct messages | `messageAdded` | Message receivers receive new message events | ### Best Practices 1. **Reconnect on Disconnect**: Implement automatic reconnection with exponential backoff 2. **Handle Auth Errors**: If authentication fails, re-authenticate and retry 3. **Filter Client-Side**: Even though server filters, consider additional client-side filtering if needed 4. **Connection Management**: Close subscriptions when no longer needed to free resources 5. **Error Handling**: Always handle subscription errors gracefully --- ## Data Types Reference ### Scalar Types | Type | Description | |------|-------------| | `ID` | Unique identifier (string representation) | | `String` | UTF-8 string | | `Boolean` | true or false | ### Enum Values **Task Priority**: `low`, `medium`, `high` (string values) **Task Status Codes**: Customizable (e.g., `open`, `in_progress`, `done`) ### Date/Time Format All timestamps use **ISO 8601 / RFC 3339** format: ``` 2024-01-15T10:30:00Z ``` --- ## Quick Reference ### Authentication Header ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` ### Typical Client Flow 1. **Login** → Obtain JWT token 2. **Store Token** → Securely persist the token 3. **Authenticated Requests** → Include token in all subsequent requests 4. **Handle Errors** → Check for authentication/authorization errors 5. **Token Refresh** → Token has long expiration (10 years), but handle expiry if needed ### Permission Check Pattern Before performing privileged operations, verify the user has the required permission by checking the `permissions` array in the JWT claims or the user's roles. --- ## Implementation Tips ### Language-Agnostic Considerations 1. **Use a GraphQL Client Library**: Most languages have mature GraphQL clients (Apollo, urql, gql-request, etc.) 2. **Handle JWT Securely**: Store tokens securely; never in localStorage for web apps (use httpOnly cookies or secure storage) 3. **Implement Retry Logic**: For network failures, implement exponential backoff 4. **Cache Responses**: Use client-side caching to reduce redundant queries 5. **Batch Requests**: Combine multiple queries in a single request when possible ### Example HTTP Request ```http POST /query HTTP/1.1 Host: api.example.com Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "query": "query { users { id email } }" } ``` ### Example cURL Command ```bash curl -X POST https://api.example.com/query \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{"query": "query { users { id email } }"}' ``` --- ## MCP Server The ARP server also exposes a **Model Context Protocol (MCP)** interface for AI agent integration. MCP allows AI assistants to discover and use the GraphQL API through a standardized tool interface. ### MCP Endpoint - **Protocol**: HTTP with Server-Sent Events (SSE) - **Endpoint**: `/mcp` (SSE connection endpoint) - **Message Endpoint**: `/message` (for sending JSON-RPC messages) - **Protocol Version**: `2024-11-05` ### MCP Authentication All MCP operations require authentication. The authentication flow is: 1. **Obtain JWT Token**: First, authenticate via the GraphQL `login` mutation to get a JWT token 2. **Include Token in SSE Connection**: Pass the `Authorization` header when establishing the SSE connection to `/mcp` 3. **Session Maintains Auth**: The user context is stored in the session, so subsequent `/message` requests inherit authentication #### Authentication Header Include the JWT token when connecting to the MCP endpoint: ```http GET /mcp HTTP/1.1 Host: api.example.com Accept: text/event-stream Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` #### Authenticated Connection Flow ``` ┌─────────────────────────────────────────────────────────────────┐ │ 1. Client: POST /query { login(email, password) } │ │ ↓ Response: { token: "jwt..." } │ │ │ │ 2. Client: GET /mcp Authorization: Bearer jwt... │ │ ↓ Server validates token, creates session with user context │ │ ↓ Server sends: event: endpoint, data: /message?sessionId=X │ │ │ │ 3. Client: POST /message?sessionId=X { method: "tools/call" } │ │ ↓ Server uses stored user context for authorization │ │ ↓ Response sent via SSE event stream │ └─────────────────────────────────────────────────────────────────┘ ``` #### JavaScript Example: Authenticated MCP Client ```javascript // Step 1: Login to get JWT token async function login(email, password) { const response = await fetch('/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: 'mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) { token } }', variables: { email, password } }) }); const data = await response.json(); return data.data.login.token; } // Step 2: Establish authenticated SSE connection function createMCPConnection(token) { const eventSource = new EventSource('/mcp', { headers: { 'Authorization': `Bearer ${token}` } }); // Note: EventSource doesn't support headers natively // Use a library like 'event-source-polyfill' or fetch-based SSE return eventSource; } // Alternative: Using fetch-based SSE with authentication async function createAuthenticatedMCP(token) { const response = await fetch('/mcp', { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'text/event-stream' } }); const reader = response.body.getReader(); const decoder = new TextDecoder(); return { reader, decoder }; } ``` #### Using event-source-polyfill Library ```javascript import { EventSourcePolyfill } from 'event-source-polyfill'; async function connectMCP() { // First, login to get token const token = await login('user@example.com', 'password'); // Create authenticated SSE connection const eventSource = new EventSourcePolyfill('/mcp', { headers: { 'Authorization': `Bearer ${token}` } }); let messageEndpoint = null; eventSource.addEventListener('endpoint', (event) => { messageEndpoint = event.data; console.log('Message endpoint:', messageEndpoint); // Initialize the MCP session initializeMCP(messageEndpoint); }); eventSource.addEventListener('message', (event) => { const response = JSON.parse(event.data); console.log('MCP response:', response); }); eventSource.onerror = (error) => { console.error('SSE error:', error); }; } async function initializeMCP(messageEndpoint) { await fetch(messageEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'my-client', version: '1.0.0' } } }) }); } ``` #### Authentication Errors If authentication fails, you'll receive an error when trying to execute operations: ```json { "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "unauthorized: authentication required" } ], "isError": true } } ``` #### Subscription Authentication MCP resource subscriptions (for real-time updates) also require authentication. The subscription endpoints check for user context before allowing subscriptions: ```json { "jsonrpc": "2.0", "id": 8, "method": "resources/subscribe", "params": { "uri": "graphql://subscription/taskCreated" } } ``` If not authenticated: ```json { "jsonrpc": "2.0", "id": 8, "error": { "code": -32603, "message": "Authentication required for subscriptions" } } ``` ### Available Tools The MCP server exposes three tools: | Tool | Description | |------|-------------| | `introspect` | Discover the GraphQL schema - types, fields, queries, mutations | | `query` | Execute GraphQL queries (read operations) | | `mutate` | Execute GraphQL mutations (create/update/delete operations) | ### Connection Flow 1. **Establish SSE Connection**: Connect to `/mcp` endpoint 2. **Receive Endpoint URL**: Server sends an `endpoint` event with the message URL 3. **Initialize**: Send an `initialize` request via the message endpoint 4. **Call Tools**: Use `tools/call` to execute introspect, query, or mutate ### MCP Protocol Messages #### Initialize Request ```json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "my-client", "version": "1.0.0" } } } ``` #### Initialize Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": { "listChanged": false } }, "serverInfo": { "name": "ARP MCP Server", "version": "1.0.0" }, "instructions": "Use the introspect tool to discover the GraphQL schema..." } } ``` #### List Tools Request ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } ``` #### List Tools Response ```json { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "introspect", "description": "Get GraphQL schema information...", "inputSchema": { "type": "object", "properties": { "typeName": { "type": "string", "description": "Optional - specific type to introspect" } } } }, { "name": "query", "description": "Execute GraphQL queries...", "inputSchema": { "type": "object", "properties": { "query": { "type": "string" }, "variables": { "type": "object" } }, "required": ["query"] } }, { "name": "mutate", "description": "Execute GraphQL mutations...", "inputSchema": { "type": "object", "properties": { "mutation": { "type": "string" }, "variables": { "type": "object" } }, "required": ["mutation"] } } ] } } ``` ### Tool Usage Examples #### Introspect Tool Get full schema overview: ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "introspect", "arguments": {} } } ``` Get specific type information: ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "introspect", "arguments": { "typeName": "User" } } } ``` #### Query Tool Execute a GraphQL query: ```json { "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "query", "arguments": { "query": "query { users { id email roles { name } } }" } } } ``` With variables: ```json { "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "query", "arguments": { "query": "query User($id: ID!) { user(id: $id) { id email } }", "variables": { "id": "1" } } } } ``` #### Mutate Tool Execute a GraphQL mutation: ```json { "jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": { "name": "mutate", "arguments": { "mutation": "mutation CreateUser($input: NewUser!) { createUser(input: $input) { id email } }", "variables": { "input": { "email": "newuser@example.com", "password": "securepassword", "roles": [] } } } } } ``` ### Authentication The MCP server inherits authentication from the SSE connection. If you need authenticated operations: 1. First obtain a JWT token via the GraphQL `login` mutation 2. Include the token when establishing the SSE connection or in request headers ### Example: JavaScript MCP Client ```javascript // Establish SSE connection const eventSource = new EventSource('/mcp'); eventSource.addEventListener('endpoint', (event) => { const messageUrl = event.data; console.log('Message endpoint:', messageUrl); // Initialize the connection fetch(messageUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'js-client', version: '1.0.0' } } }) }); }); eventSource.addEventListener('message', (event) => { const response = JSON.parse(event.data); console.log('Received:', response); }); // Call a tool async function callTool(name, arguments) { const response = await fetch('/message?sessionId=YOUR_SESSION_ID', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method: 'tools/call', params: { name, arguments } }) }); return response.json(); } // Usage await callTool('query', { query: '{ users { id email } }' }); ``` ### MCP Use Cases | Use Case | Tool | Example | |----------|------|---------| | Discover API structure | `introspect` | Get schema before making queries | | Read user data | `query` | Fetch users, tasks, messages | | Create new records | `mutate` | Create users, tasks, notes | | Update existing records | `mutate` | Update task status, user info | | Delete records | `mutate` | Remove tasks, notes, users | ### MCP Resources (Subscriptions) The MCP server also supports **resources** for real-time subscriptions. Resources allow AI agents to subscribe to GraphQL subscription events through the MCP protocol. #### Available Resources | Resource URI | Description | |--------------|-------------| | `graphql://subscription/taskCreated` | Task creation 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) | #### List Resources Request ```json { "jsonrpc": "2.0", "id": 7, "method": "resources/list" } ``` #### List Resources Response ```json { "jsonrpc": "2.0", "id": 7, "result": { "resources": [ { "uri": "graphql://subscription/taskCreated", "name": "taskCreated", "description": "Subscribe to task creation events...", "mimeType": "application/json" } ] } } ``` #### Subscribe to Resource ```json { "jsonrpc": "2.0", "id": 8, "method": "resources/subscribe", "params": { "uri": "graphql://subscription/taskCreated" } } ``` #### Subscribe Response ```json { "jsonrpc": "2.0", "id": 8, "result": { "subscribed": true, "uri": "graphql://subscription/taskCreated" } } ``` #### Receive Resource Updates After subscribing, you'll receive notifications via the SSE connection: ```json { "jsonrpc": "2.0", "method": "notifications/resources/updated", "params": { "uri": "graphql://subscription/taskCreated", "contents": { "uri": "graphql://subscription/taskCreated", "mimeType": "application/json", "text": "{\"id\":\"5\",\"title\":\"New Task\",\"assigneeId\":\"2\",...}" } } } ``` #### Unsubscribe ```json { "jsonrpc": "2.0", "id": 9, "method": "resources/unsubscribe", "params": { "uri": "graphql://subscription/taskCreated" } } ``` #### Subscription Example ```javascript // Subscribe to task creation events async function subscribeToTasks(sessionId) { const response = await fetch(`/message?sessionId=${sessionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'resources/subscribe', params: { uri: 'graphql://subscription/taskCreated' } }) }); return response.json(); } // Listen for notifications on the SSE connection eventSource.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.method === 'notifications/resources/updated') { const task = JSON.parse(data.params.contents.text); console.log('New task:', task); } }); ``` ### MCP Best Practices 1. **Introspect First**: Always call `introspect` to understand the schema before making queries 2. **Handle Errors**: Check for `isError: true` in tool results 3. **Use Variables**: Pass variables separately for better readability and security 4. **Session Management**: Maintain the SSE connection for the duration of your session 5. **Reconnect on Disconnect**: Implement reconnection logic for long-running sessions 6. **Use Resources for Real-time**: Subscribe to resources for real-time updates instead of polling --- ## Workflow Engine The workflow engine enables you to define complex multi-step processes as JSON-based DAGs (Directed Acyclic Graphs). Each node in the DAG represents a task or decision point, and dependencies between nodes are automatically resolved. ### Workflow Types | Type | Description | |------|-------------| | `task` | A task node that creates a task for a user | | `condition` | A decision node that evaluates conditions | | `parallel` | A node that spawns parallel branches | | `join` | A node that waits for multiple branches to complete | | `trigger` | A node that starts the workflow | ### Workflow Permissions | Operation | Required Permission | |-----------|---------------------| | `createWorkflowTemplate` | `workflow:create` | | `updateWorkflowTemplate` | `workflow:manage` | | `deleteWorkflowTemplate` | `workflow:manage` | | `startWorkflow` | `workflow:start` | | `cancelWorkflow` | `workflow:manage` | | `retryWorkflowNode` | `workflow:intervene` | | `workflowTemplates` query | `workflow:view` | | `workflowInstance` query | `workflow:view` | ### Workflow Definition Format Workflows are defined as JSON with a `nodes` object where each key is a unique node identifier: ```json { "nodes": { "start": { "type": "task", "title": "Initial Review", "content": "Review the initial request", "assignee": "1", "dependsOn": [] }, "analysis": { "type": "task", "title": "Data Analysis", "content": "Analyze the data", "assignee": "2", "dependsOn": ["start"] }, "approval": { "type": "task", "title": "Manager Approval", "content": "Get manager approval", "assignee": "3", "dependsOn": ["analysis"] }, "end": { "type": "task", "title": "Complete", "content": "Mark workflow as complete", "assignee": "1", "dependsOn": ["approval"] } } } ``` ### Workflow API Examples #### Create a Workflow Template ```graphql mutation CreateWorkflowTemplate($input: NewWorkflowTemplate!) { createWorkflowTemplate(input: $input) { id name description definition isActive createdAt } } ``` **Variables:** ```json { "input": { "name": "Onboarding", "description": "New employee onboarding workflow", "definition": "{\"nodes\":{\"start\":{\"type\":\"task\",\"title\":\"Welcome\",\"content\":\"Send welcome email\",\"assignee\":\"1\",\"dependsOn\":[]},\"setup\":{\"type\":\"task\",\"title\":\"Setup Account\",\"content\":\"Create user account\",\"assignee\":\"2\",\"dependsOn\":[\"start\"]}}}", "isActive": true } } ``` #### Start a Workflow Instance ```graphql mutation StartWorkflow($templateId: ID!, $input: StartWorkflowInput!) { startWorkflow(templateId: $templateId, input: $input) { id status context template { id name } service { id name } createdAt } } ``` **Variables:** ```json { "templateId": "1", "input": { "serviceId": "1", "context": "New hire onboarding" } } ``` #### Get All Workflow Instances ```graphql query WorkflowInstances { workflowInstances { id status context template { id name } service { id name } createdAt completedAt } } ``` #### Get Single Workflow Instance ```graphql query WorkflowInstance($id: ID!) { workflowInstance(id: $id) { id status context template { id name definition } service { id name } createdAt completedAt } } ``` #### Cancel a Workflow ```graphql mutation CancelWorkflow($id: ID!) { cancelWorkflow(id: $id) { id status completedAt } } ``` #### Retry a Failed Node ```graphql mutation RetryWorkflowNode($nodeId: ID!) { retryWorkflowNode(nodeId: $nodeId) { id nodeKey nodeType status task { id title } retryCount } } ``` ### MCP Tools for Workflows The MCP server also supports workflow operations through the `query` and `mutate` tools. #### Query Workflow Templates ```json { "jsonrpc": "2.0", "id": 10, "method": "tools/call", "params": { "name": "query", "arguments": { "query": "query { workflowTemplates { id name isActive } }" } } } ``` #### Start Workflow via MCP ```json { "jsonrpc": "2.0", "id": 11, "method": "tools/call", "params": { "name": "mutate", "arguments": { "mutation": "mutation StartWorkflow($templateId: ID!, $input: StartWorkflowInput!) { startWorkflow(templateId: $templateId, input: $input) { id status } }", "variables": { "templateId": "1", "input": { "serviceId": "1", "context": "Workflow started via MCP" } } } } } ``` ### Workflow Node States | State | Description | |-------|-------------| | `pending` | Node is waiting for dependencies to complete | | `ready` | Node is ready to execute (all dependencies satisfied) | | `running` | Node is currently executing | | `completed` | Node has completed successfully | | `failed` | Node has failed (may be retried) | | `skipped` | Node was skipped | ### Automatic Task Completion Integration When a task associated with a workflow node is marked as "done", the workflow engine automatically: 1. **Marks the node as completed** - The workflow node status changes to `completed` 2. **Evaluates downstream dependencies** - Checks which nodes are now ready to execute 3. **Creates tasks for newly ready nodes** - Automatically creates tasks for nodes whose dependencies are satisfied This enables workflows to progress automatically as users complete their assigned tasks without manual intervention. ### Retry Logic Workflows support automatic retry with configurable max retries: - **Default Max Retries**: 3 - **Retry Delay**: 60 seconds (configurable via `WORKFLOW_RETRY_DELAY`) To manually retry a failed node: ```graphql mutation RetryWorkflowNode($nodeId: ID!) { retryWorkflowNode(nodeId: $nodeId) { id nodeKey status retryCount } } ``` ### Best Practices 1. **Design DAGs Carefully**: Ensure workflows have clear start and end points 2. **Use Meaningful Node Keys**: Node keys should be descriptive and unique 3. **Handle Failures**: Design workflows to handle node failures gracefully 4. **Monitor Workflow Progress**: Use `workflowInstances` query to track progress 5. **Set Proper Assignees**: Ensure each task node has an appropriate assignee 6. **Test Workflows**: Test workflows with sample data before production use