# wl-registry

Python SDK for [Watchlight AI Registry](https://www.watchlight.ai) — AI agent and MCP server registry.

Register, discover, and manage AI agents, MCP servers, scanners, and permissions through a type-safe Python client.

## Installation

```bash
pip install wl-registry
```

## Quick Start

### Public Client (No Auth)

```python
from wl_registry import WlRegistryClient, SelfRegisterAgentRequest

# Check service health
async with WlRegistryClient("http://localhost:8080/api/v1") as client:
    healthy = await client.health()

    # Self-register an agent
    resp = await client.register_agent(
        SelfRegisterAgentRequest(
            name="my-agent",
            agent_type="autonomous",
            description="My autonomous agent",
        )
    )
    print(f"Agent ID: {resp.id}, API Key: {resp.api_key}")
```

### Scanner Client

Register and manage MCP servers discovered by scanners:

```python
from wl_registry import WlRegistryScannerClient, RegisterServerRequest

async with WlRegistryScannerClient(
    scanner_id="scanner-uuid",
    api_key="scanner-api-key",
    base_url="http://localhost:8080/api/v1",
) as client:
    resp = await client.register_server(
        RegisterServerRequest(
            host_id="host-1",
            address="http://localhost:4000",
            transport="http",
            server_name="my-mcp-server",
            lease_ttl_seconds=300,
        )
    )
    print(f"Server {resp.server_id} registered")

    # Keep alive
    await client.heartbeat(resp.server_id)
```

### Agent Client

Query accessible servers from the agent's perspective:

```python
from wl_registry import WlRegistryAgentClient

async with WlRegistryAgentClient(
    agent_id="agent-uuid",
    api_key="agent-api-key",
) as client:
    servers = await client.get_accessible_servers()
    for srv in servers.servers:
        print(f"Can access: {srv.server_name} at {srv.address}")

    await client.heartbeat()
```

### Admin Client

Full CRUD operations for managing the registry:

```python
from wl_registry import (
    WlRegistryAdminClient,
    CreateAgentRequest,
    GrantPermissionRequest,
)

async with WlRegistryAdminClient(admin_key="admin-key") as client:
    # List all agents
    agents = await client.list_agents()
    print(f"Total agents: {agents.total}")

    # Create an agent
    agent = await client.create_agent(
        CreateAgentRequest(
            name="new-agent",
            agent_type="workflow",
            description="A workflow orchestrator",
        )
    )

    # Grant permission to access a server
    await client.grant_permission(
        agent.agent_id,
        GrantPermissionRequest(
            server_id="srv-uuid",
            allowed_tools=["read_file", "search"],
        ),
    )

    # Get agent statistics
    stats = await client.get_agent_stats()
    print(f"Active: {stats.active_agents}, Trusted: {stats.trusted_agents}")
```

## Synchronous Clients

All clients have synchronous counterparts:

```python
from wl_registry import WlRegistrySyncClient, WlRegistryAdminSyncClient

# Public
with WlRegistrySyncClient() as client:
    healthy = client.health()

# Admin
with WlRegistryAdminSyncClient(admin_key="key") as client:
    agents = client.list_agents()
```

Also available: `WlRegistryScannerSyncClient`, `WlRegistryAgentSyncClient`.

## Error Handling

All exceptions inherit from `WlRegistryError`:

```python
from wl_registry import (
    WlRegistryError,
    AuthenticationError,
    NotFoundError,
    ValidationError,
    ConflictError,
    ServiceUnavailable,
)

try:
    agent = await client.get_agent("nonexistent")
except NotFoundError as e:
    print(f"Agent not found: {e}")
except AuthenticationError:
    print("Invalid credentials")
except WlRegistryError as e:
    print(f"Registry error: {e}")
```

| Exception | HTTP Status | Description |
|-----------|-------------|-------------|
| `AuthenticationError` | 401, 403 | Invalid or missing credentials |
| `NotFoundError` | 404 | Resource not found |
| `ValidationError` | 400, 422 | Invalid request data |
| `ConflictError` | 409 | Resource conflict (e.g., duplicate name) |
| `ServiceUnavailable` | Connection error | Service unreachable |

## Client Reference

| Client | Auth | Key Methods |
|--------|------|-------------|
| `WlRegistryClient` | None | `health`, `get_registration_config`, `register_agent` |
| `WlRegistryScannerClient` | Scanner | `register_server`, `heartbeat`, `update_trust` |
| `WlRegistryAgentClient` | Agent | `get_accessible_servers`, `heartbeat` |
| `WlRegistryAdminClient` | Admin | Full CRUD: servers, agents, permissions, scanners, tokens |

## Requirements

- Python 3.10+
- httpx >= 0.27.0
- pydantic >= 2.0.0

## License

Proprietary. See [LICENSE](LICENSE) for details.
