CLIENT_GUIDE.md 34 KB

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
  2. Connection & Endpoint
  3. Authentication
  4. Authorization & Permissions
  5. GraphQL Operations
  6. Error Handling
  7. Subscriptions
  8. 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

{
  "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

mutation Login($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    token
    user {
      id
      email
      roles {
        id
        name
        permissions {
          id
          code
          description
        }
      }
    }
  }
}

Variables:

{
  "email": "user@example.com",
  "password": "your-password"
}

Response:

{
  "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>

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:

{
  "errors": [
    {
      "message": "unauthorized: authentication required"
    }
  ]
}

Or for missing permissions:

{
  "errors": [
    {
      "message": "unauthorized: missing user:update permission"
    }
  ]
}

GraphQL Operations

Queries (Read Operations)

All queries require authentication.

Users

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

mutation Login($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    token
    user { id email }
  }
}

Users

# 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

# 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

# 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

# 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

# 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

# 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

# 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

# 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:

{
  "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

# 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

{
  "type": "connection_init",
  "payload": {
    "Authorization": "Bearer <token>"
  }
}

Subscribe to a Topic

{
  "id": "1",
  "type": "start",
  "payload": {
    "query": "subscription { taskCreated { id title assigneeId } }"
  }
}

Receive Events

{
  "id": "1",
  "type": "data",
  "payload": {
    "data": {
      "taskCreated": {
        "id": "5",
        "title": "New Task",
        "assigneeId": "2"
      }
    }
  }
}

Unsubscribe

{
  "id": "1",
  "type": "stop"
}

Example: JavaScript/TypeScript Client

Using the graphql-ws library:

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:

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

POST /query HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{
  "query": "query { users { id email } }"
}

Example cURL Command

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:

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

// 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

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:

{
  "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:

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "resources/subscribe",
  "params": {
    "uri": "graphql://subscription/taskCreated"
  }
}

If not authenticated:

{
  "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

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {
      "name": "my-client",
      "version": "1.0.0"
    }
  }
}

Initialize Response

{
  "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

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}

List Tools Response

{
  "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:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "introspect",
    "arguments": {}
  }
}

Get specific type information:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "introspect",
    "arguments": {
      "typeName": "User"
    }
  }
}

Query Tool

Execute a GraphQL query:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "query",
    "arguments": {
      "query": "query { users { id email roles { name } } }"
    }
  }
}

With variables:

{
  "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:

{
  "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

// 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

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "resources/list"
}

List Resources Response

{
  "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

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "resources/subscribe",
  "params": {
    "uri": "graphql://subscription/taskCreated"
  }
}

Subscribe Response

{
  "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:

{
  "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

{
  "jsonrpc": "2.0",
  "id": 9,
  "method": "resources/unsubscribe",
  "params": {
    "uri": "graphql://subscription/taskCreated"
  }
}

Subscription Example

// 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