Metadata-Version: 2.4
Name: pCloudy-mcp
Version: 0.1.4
Summary: MCP server for pCloudy
Author: pCloudy
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Requires-Dist: diskcache>=5.6.3
Requires-Dist: fastmcp>=2.5.1
Requires-Dist: httpx>=0.24.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: python-socketio==5.16.1
Requires-Dist: tenacity>=9.1.2
Requires-Dist: websocket-client>=1.9.0
Provides-Extra: dev
Requires-Dist: black>=25.1.0; extra == 'dev'
Requires-Dist: isort>=6.0.1; extra == 'dev'
Requires-Dist: mypy>=1.16.1; extra == 'dev'
Requires-Dist: poethepoet>=0.24.4; extra == 'dev'
Requires-Dist: pytest>=8.4.1; extra == 'dev'
Requires-Dist: ruff>=0.4.4; extra == 'dev'
Description-Content-Type: text/markdown

# pCloudy MCP Server

> Connect AI agents to real devices and QPilot automation — via the **Model Context Protocol**.

---

## What is MCP?

**MCP (Model Context Protocol)** is an open standard that lets AI tools (Claude, Cursor, VS Code Copilot, etc.) call external tools and APIs in a structured, vendor-neutral way. Think of it as a USB-C port for AI — one protocol, any device.

- Plug AI assistants into real APIs without writing glue code
- Switch LLM vendors with zero changes to your tools
- Keep data and credentials inside your own infrastructure

[Learn more about MCP](https://modelcontextprotocol.io/introduction)

---

## What does pCloudy MCP do?

`pCloudy-MCP` is an MCP server that bridges AI agents to the pCloudy Device Cloud and QPilot test automation engine. With it, an AI agent can:

- Book and release real Android/iOS devices or cloud browsers
- Upload and install apps
- Create, manage, and execute QPilot test cases using natural language
- Inspect test step results and debug failures in real-time

---

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────────┐
│                        AI Client (Claude / Cursor / etc.)       │
└────────────────────────────┬────────────────────────────────────┘
                             │  MCP Protocol (stdio / SSE)
┌────────────────────────────▼────────────────────────────────────┐
│                     pCloudy MCP Server                          │
│                                                                 │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐   │
│  │ Device Cloud │  │ Browser Cloud│  │     QPilot Tools     │   │
│  │   Tools      │  │   Tools      │  │  (v2 architecture)   │   │
│  └──────┬───────┘  └──────┬───────┘  └──────────┬───────────┘   │
│         │                 │                       │             │
│  ┌──────▼───────┐  ┌──────▼───────┐  ┌──────────▼───────────┐   │
│  │  Device API  │  │  Browser API │  │     QPilot API v2     │  │
│  └──────┬───────┘  └──────┬───────┘  └──────────┬───────────┘   │
└─────────┼─────────────────┼────────────────────  ┼─────────────┘
          │                 │                       │
          ▼                 ▼                       ▼
   pCloudy Device     pCloudy Browser       QPilot Backend
      Cloud REST        Cloud REST         REST + WebSocket
```

---

## QPilot v2 — How It Works

QPilot v2 introduced a **real-time WebSocket playground model**. Each test execution is a live session, not a batch job. Here is the full lifecycle:

```
┌──────────────────────────────────────────────────────────────────┐
│                    QPilot Session Lifecycle                      │
│                                                                  │
│  1. create_qpilot_playground_session                             │
│     │                                                            │
│     ├─► Books device / browser via pCloudy                       │
│     ├─► Opens Socket.IO connection to QPilot backend             │
│     ├─► Receives req_play_id + context_id                        │
│     └─► Writes session file: .qpilot-session-details/<test_id>/  │
│                                                                  │
│  2. add_qpilot_step / insert_qpilot_step / retry_qpilot_step     │
│     │                                                            │
│     ├─► Calls QPilot REST API to submit the step                 │
│     └─► Waits for step result via 3-path resolution:             │
│                                                                  │
│         Path 1 (fastest) ── In-memory event list                 │
│                              Written by WebSocket listener       │
│                                                                  │
│         Path 2 (fallback) ─ .playground_events.jsonl file        │
│                              Written in parallel by the socket   │
│                                                                  │
│         Path 3 (120s+) ──── REST API polling                     │
│                              get_test_list until terminal status │
│                                                                  │
│         → First path to return a terminal status wins.           │
│           Other paths are cancelled via stop_event.              │
│                                                                  │
│  3. close_qpilot_playground_session                              │
│     │                                                            │
│     ├─► Calls finish_test API                                    │
│     ├─► Closes Socket.IO connection                              │
│     └─► Cleans up local session files                            │
└──────────────────────────────────────────────────────────────────┘
```

### Step Status Resolution (in detail)

When a step is submitted, pCloudy MCP immediately spawns three concurrent workers:

| Worker | Mechanism | Activates |
|--------|-----------|-----------|
| **Memory** | Reads events pushed by the live WebSocket connection | Instantly |
| **File** | Tails `.playground_events.jsonl` written by the socket | Within 60s of file appearing |
| **API Poller** | Polls `GET /api/v2/test/fetch` with the test ID | After 120s timeout guard |

The first worker to report a **terminal status** (`passed`, `failed`, `error`, `skipped`, `Aborted`) wins. The other two are stopped via a shared `threading.Event`.

---

## Project Structure

```
src/pcloudy_mcp/
├── main.py                         # CLI entry point — loads config, sets up logging, starts server
├── server.py                       # FastMCP server factory and lifecycle
│
├── api/                            # Raw HTTP wrappers (one class per domain)
│   ├── http_client.py              # Base async HTTPX client with retry + token masking
│   ├── authenticate.py             # Token fetch + cache
│   ├── device_booking.py           # Device book/release/live-view
│   ├── browser_booking.py          # Browser VM book/view
│   └── qpilot.py                   # QPilot v2 REST endpoints
│
├── socket/
│   └── qpilot/
│       ├── qpilot_ws_client.py     # Socket.IO client (PlaygroundSocket)
│       └── qpilot_ws_connection.py # Session open/close/send, _active_sessions registry
│
├── tools/                          # MCP tool definitions (registered into FastMCP)
│   ├── device_booking.py           # Device cloud tools
│   ├── browser_booking.py          # Browser cloud tools
│   ├── app_management.py           # App upload/install tools
│   └── qpilot.py                   # All QPilot tools (23 tools)
│
├── utils/
│   ├── config.py                   # Env var loading via Pydantic BaseSettings
│   ├── file_utils.py               # Path helpers (project root, ensure_path)
│   ├── qpilot_utils.py             # get_step_status (3-path resolution), session helpers
│   └── tools_response_format.py   # Standardised MCP response builder
│
├── Constant/
│   └── constant.py                 # All API endpoints, QPilot server routing, retry config
│
├── logger/
│   └── logger.py                   # Custom VERBOSE log level (level 5), setup_logging()
│
├── validation/
│   └── validation.py               # Pydantic settings model — validates all env vars
│
└── errors/
    └── exception.py                # McpToolError, ServerInitializationError
```

---

## Prerequisites

1. **pCloudy account** — [Sign up here](https://device.pcloudy.com/signup)
2. **Python ≥ 3.10**
3. **uv** — [Install uv](https://docs.astral.sh/uv/getting-started/installation/)

---

## Configuration

### Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `PCLOUDY_USERNAME` | Yes | Your pCloudy account email |
| `PCLOUDY_API_KEY` | Yes | API key from your pCloudy account settings |
| `PCLOUDY_CLOUD_URL` | Yes | Your pCloudy instance URL e.g. `https://device.pcloudy.com` |
| `PCLOUDY_VERBOSE_LOGS` | No | Set to `true` to enable verbose logging (default: `false`) |

### Claude Desktop (`claude_desktop_config.json`)

```json
{
  "mcpServers": {
    "pCloudy": {
      "command": "uvx",
      "args": ["pcloudy-mcp"],
      "env": {
        "PCLOUDY_USERNAME": "<your_pcloudy_username>",
        "PCLOUDY_API_KEY": "<your_pcloudy_api_key>",
        "PCLOUDY_CLOUD_URL": "<your_pcloudy_cloud_url>"
      }
    }
  }
}
```

### Cursor (`mcp.json`)

```json
{
  "mcpServers": {
    "pCloudy": {
      "command": "uvx",
      "args": ["pcloudy-mcp"],
      "env": {
        "PCLOUDY_USERNAME": "<your_pcloudy_username>",
        "PCLOUDY_API_KEY": "<your_pcloudy_api_key>",
        "PCLOUDY_CLOUD_URL": "<your_pcloudy_cloud_url>"
      }
    }
  }
}
```

---

## Tool Reference

### Device Cloud

| Tool | Description |
|------|-------------|
| `get_device_list` | List available real devices filtered by platform (android / ios) |
| `get_device_details` | Get detailed information about a specific device by ID |
| `book_device` | Book a device by name for a testing session |
| `release_device` | Release a booked device using its booking ID (RID) |
| `get_live_view_url` | Get the live view URL for a currently booked device |
| `refresh_authentication` | Refresh the user authentication token |

### App Management

| Tool | Description |
|------|-------------|
| `get_all_application` | List all apps uploaded to the pCloudy account |
| `upload_app` | Upload an APK or IPA file to pCloudy |
| `install_app` | Install an uploaded app on a booked device |
| `resign_app` | Re-sign an IPA to make it installable on a specific iOS device |

### Browser Cloud

| Tool | Description |
|------|-------------|
| `get_browser_list` | List available browser VMs by machine type |
| `book_browser` | Book a cloud browser session on a specified VM |
| `get_browser_live_view_url` | Get the live view URL for a booked browser session |
| `release_browser` | Release a booked browser session |

### QPilot — Project & Test Management

| Tool | Description |
|------|-------------|
| `get_qpilot_projects` | List all owned QPilot projects |
| `create_qpilot_project` | Create a new QPilot project |
| `delete_qpilot_project` | Delete a QPilot project by name |
| `get_qpilot_folders` | List all folders across owned projects |
| `create_qpilot_folder` | Create a new folder inside a project |
| `delete_qpilot_folder` | Delete a folder by name |
| `get_qpilot_tests` | List all tests with step counts, grouped by project and folder |
| `create_qpilot_test` | Create a new test for android, ios, or web |
| `delete_qpilot_test` | Delete a test by name |

### QPilot — Playground Session

| Tool | Description |
|------|-------------|
| `create_qpilot_playground_session` | Open a live WebSocket session for a test; books the required device or browser |
| `close_qpilot_playground_session` | Close an active session, release the device/browser, and clean up local files |

### QPilot — Step Operations

| Tool | Description |
|------|-------------|
| `add_qpilot_step` | Append a natural language step and block until it reaches a terminal status |
| `insert_qpilot_step` | Insert a step at a specific 0-based index, shifting later steps forward |
| `delete_qpilot_step` | Delete the step at a specific 0-based index |
| `get_qpilot_test_steps` | List all steps with their index, content, status, and AI reasoning |
| `retry_qpilot_step` | Retry a single step by its 0-based index |
| `retry_qpilot_steps_from` | Retry a step and all subsequent steps (cascade retry) |
| `restart_qpilot_test` | Re-execute all steps from the beginning |

### QPilot — Test Data

> Test data is a JSON key-value store attached to a test, used to inject dynamic values into steps.

| Tool | Description |
|------|-------------|
| `get_qpilot_test_data` | Retrieve all key-value pairs stored on a test |
| `add_qpilot_test_data` | Add new keys or overwrite existing ones with a JSON object |
| `modify_qpilot_test_data` | Update values for existing keys only (cannot add new keys) |
| `delete_qpilot_test_data` | Remove specific keys; at least one key must remain |

---

## Typical QPilot Workflow

```
1. Create project and folder (once per project)
   get_qpilot_projects → create_qpilot_project → create_qpilot_folder

2. Create a test
   create_qpilot_test (platform: android/ios/web, folder_id, app info)

3. (Optional) Pre-populate test data
   add_qpilot_test_data → { "username": "demo@example.com", "password": "secret" }

4. Open a session
   get_device_list → create_qpilot_playground_session (test_id, device_id)

5. Add steps one at a time (each call blocks until the step completes)
   add_qpilot_step → "Tap on Login button"
   add_qpilot_step → "Enter username from test data"
   add_qpilot_step → "Verify home screen is visible"

6. Inspect, retry, or fix steps
   get_qpilot_test_steps     ← see all steps + status + reasoning
   retry_qpilot_step         ← retry step 2 only
   retry_qpilot_steps_from   ← retry from step 2 onwards

7. Close and save
   close_qpilot_playground_session
```

---

## Verbose Logging

Set `PCLOUDY_VERBOSE_LOGS=true` to enable detailed logs including:

- WebSocket connection lifecycle (connect, disconnect, booking details)
- Session open/close events with `req_play_id` and `context_id`
- Step index calculations and submission
- API polling activation (when WS/file paths take > 120s)

```json
"env": {
  "PCLOUDY_VERBOSE_LOGS": "true"
}
```

Log levels in order of verbosity: `VERBOSE (5)` → `DEBUG` → `INFO` → `ERROR`

> **Security note:** Tokens and API keys are always masked as `*************` in logs, regardless of log level.

---

## Development

```bash
git clone https://github.com/Smart-Software-Testing-Solutions-Opkey/pcloudy-mcp-server.git
cd pcloudy-mcp-server
uv pip install .[dev]
```

### Developer commands

```bash
poe format        # black + isort
poe lint-check    # ruff linter
poe typecheck     # mypy
poe check-server  # verify server starts correctly
```

### Adding a new tool

1. Create or extend a file in `src/pcloudy_mcp/tools/`
2. Define a function decorated with `@mcp_instance.tool(name=..., description=...)`
3. Register it in the tool config (see `utils/tools_auto_registry.py`)
4. Add corresponding API methods to `src/pcloudy_mcp/api/` if needed
5. Add the endpoint constant to `Constant/constant.py`

---

## Contributors

Maintained by the pCloudy / SSTS team.
