Skip to main content
Glama
README.md
# selvans

**selvans** is a framework that makes any web application natively operable by AI agents through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/).

Instead of using browser automation, AI agents connect directly to your application through a structured protocol — observing the UI semantic tree and calling typed tools to interact with it.

---

## How It Works

```
 External AI (Claude Desktop, Cursor…)
          │  MCP / SSE
          ▼
  ┌──────────────────┐
  │  selvans-core │  ← Hub service (WebSocket + MCP server + admin UI)
  └──────┬─────┬─────┘
         │ WS  │ WS
    ┌────┘     └────┐
    ▼               ▼
Angular App    Python Backend
(selvans-   (selvans-
  angular)       python)
```

1. **selvans-core** is the central hub. It exposes an MCP server (SSE) that external AI clients connect to, and a WebSocket endpoint that your frontend and backend connect to.
2. **selvans-angular** integrates into your Angular app. It registers a semantic UI tree and built-in tools (`click_element`, `form_input`, `get_page_state`…) with the Core.
3. **selvans-python** integrates into your Python/FastAPI backend. It registers typed service operations with the Core — tool schemas and argument validation are derived from your type hints by the official [MCP SDK](https://github.com/modelcontextprotocol/python-sdk).
4. The AI calls tools and operations through the Core, which routes them to the right app in real time.

---

## Repository Structure

```
selvans/
├── packages/
│   ├── core/                     # Hub service + Docker image (Python module: selvans_core)
│   ├── lib/                      # Client libraries
│   │   ├── selvans-angular/      # Angular library (npm)
│   │   ├── selvans-python/       # Python client library (PyPI)
│   │   └── selvans-java/         # Java library (planned)
│   ├── demo/                     # Demo stack
│   │   ├── selvans-angular-demo/ # Angular demo app
│   │   ├── selvans-python-demo/  # FastAPI demo backend
│   │   └── docker-compose.yml    # Demo Docker Compose
│   ├── setup/                    # npm installer for the Claude plugin (WIP)
│   └── claude-plugin/            # Claude plugin: agents/commands/skills (WIP)
└── .github/workflows/            # CI (docker-images.yml, release.yml)
```

> There is **no root Nx/pnpm workspace** — packages are organized by *shippable artifact* and each one builds, tests and ships on its own.

---

## Quick Start

The demo stack runs entirely in Docker (only Docker required — no Node/pnpm on the host). The Core is consumed as a published image; until it exists on Docker Hub, build it locally first:

```bash
# 1. Build the Core image locally (one-off, until it's published to Docker Hub)
docker build -t gabrieleconsonni/selvans-core:latest packages/core

# 2. Start the stack: Core (:8080) + Python demo (:8001) + Angular dev server (:4200, live-reload)
docker compose -f packages/demo/docker-compose.yml up -d --build
```

Open http://localhost:4200 — the `selvans-panel` should show **"connected"**.

> **Live-reload**: the Angular source is bind-mounted into the container. Saving a file in `packages/demo/selvans-angular-demo/` or `packages/lib/selvans-angular/` triggers an automatic browser update (~1 s latency due to polling on Docker Desktop Windows).
>
> **First startup**: the Angular container build (in-container `npm install` + esbuild) takes ~90–180 s.

**Prod-like** (Angular built and served via nginx instead of the live-reload dev server):

```bash
docker compose -f packages/demo/docker-compose.yml --profile full up -d --build
```

> **Note:** `selvans-angular-dev` (default profile, live-reload) and `selvans-angular-demo` (profile `full`, nginx) both bind port 4200 — never run both at once.

Teardown (stop + wipe volumes):

```bash
docker compose -f packages/demo/docker-compose.yml down -v
```

See [[Getting-started]] in the wiki for the full guide and troubleshooting.

---

### Manual start

#### 1. Start the Core

```bash
# Run the Core on its own (local build)
docker compose -f packages/core/docker-compose.yml up --build
# Admin UI → http://localhost:8080/ui
# MCP SSE  → http://localhost:8080/mcp/sse
```

### 2. Integrate the Angular Frontend

```bash
npm install selvans-angular
```

```typescript
// app.module.ts
import { SelvansModule } from 'selvans-angular';

@NgModule({
  imports: [
    SelvansModule.forRoot({
      coreUrl: 'http://localhost:8080',
      appId: 'my-app'
    })
  ]
})
export class AppModule {}
```

Mark your components with semantic directives:

```html
<main [SelvansNode]="{ id: 'main', template: 'layout', description: 'Main content area' }">
  <form [SelvansNode]="{ id: 'login-form', template: 'form', description: 'User login form' }">
    <input [SelvansTarget]="'email-input'" type="email" />
    <button [SelvansTarget]="'submit-btn'">Login</button>
  </form>
</main>
```

### 3. Integrate the Python Backend

```bash
pip install selvans
```

```python
# main.py
from typing import Literal
from selvans import SelvansBeApp, SelvansBeConfig, SelvansService, operation

class TaskService(SelvansService):
    name = "tasks"
    description = "Task management"

    @operation("list", description="List tasks, optionally filtered by status")
    async def list_tasks(self, status: Literal["pending", "completed"] | None = None) -> list[dict]:
        return await db.list_tasks(status=status)

    @operation("create", description="Create a new task")
    async def create_task(self, title: str, priority: Literal["high", "medium", "low"] = "medium") -> dict:
        return await db.create_task(title, priority)

surface = SelvansBeApp(SelvansBeConfig(core_url="http://localhost:8080"))
surface.register(TaskService())
app = surface.create_app()
```

Type hints drive the AI-facing schema: `Literal[...]` becomes a JSON Schema `enum`,
`X | None` an optional field, and out-of-range arguments are rejected before your
method runs.

### 4. Connect an AI Client

Add the Core's MCP endpoint to your AI client (e.g. Claude Desktop `claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "selvans": {
      "url": "http://localhost:8080/mcp/sse"
    }
  }
}
```

The AI can now observe your app's UI and call your backend operations directly.

> When the Core runs with authentication enabled (`Selvans_AUTH_MODE=enforce`),
> the client must present a bearer token (`"headers": { "Authorization": "Bearer <token>" }`)
> and only sees the tools it is authorized for — see [Authentication](#authentication).

---

## Run the Full Demo

```bash
docker compose -f packages/demo/docker-compose.yml up -d --build
# → Core :8080 + Python demo :8001 + Angular dev container :4200 (live-reload)
```

Or prod-like, with Angular served via nginx:

```bash
docker compose -f packages/demo/docker-compose.yml --profile full up -d --build
# → Core :8080 + Python demo :8001 + Angular nginx :4200 (static bundle)
```

| Service              | URL                            | Mode                        |
|----------------------|--------------------------------|-----------------------------|
| selvans-core      | http://localhost:8080          | both                        |
| Admin UI             | http://localhost:8080/ui       | both                        |
| MCP SSE endpoint     | http://localhost:8080/mcp/sse  | both                        |
| Python demo backend  | http://localhost:8001          | both                        |
| Angular demo frontend| http://localhost:4200          | dev (live-reload) / prod-like (nginx) |

> **Note:** `selvans-angular-dev` (dev, live-reload) and `selvans-angular-demo` (profile `full`, nginx) both bind port 4200 — do not start them together.

---

## Packages

| Package | Description | Docs |
|---------|-------------|------|
| `selvans-angular` | Angular library — semantic UI tree + built-in tools | [docs](packages/lib/selvans-angular/) |
| `selvans-python` | Python library — FastAPI integration + service operations | [docs](packages/lib/selvans-python/) |
| `selvans-core` | Hub service — MCP server + WebSocket hub + admin UI | [docs](packages/core/) |

---

## Built-in Frontend Tools

The Angular library registers these tools automatically:

| Tool | Description |
|------|-------------|
| `get_page_state` | Returns current URL, title, and visible text |
| `navigate` | Navigate to a route by path |
| `get_elements` | List all elements marked with `[SelvansTarget]` |
| `click_element` | Click an element by its target ID |
| `form_input` | Read or set the value of a form field by target ID |

---

## In-App AI Chat (built-in)

External MCP clients (Claude Desktop, Cursor) run the AI in a separate window.
For a chat that lives **next to the UI it drives**, the Core ships a built-in
agent loop — no AI vendor SDK, just plain HTTP to the provider you configure.

```
        ┌──────────────── selvans-core ────────────────┐
Browser │  WS /chat/ws →  ChatOrchestrator (agent loop) │ ──HTTP──▶ LLM
 chat   │                      │ router.dispatch()      │   (Anthropic /
panel / │                      ▼                        │    OpenAI / Ollama)
console │              connected apps (FE + BE) via WS   │
        └───────────────────────────────────────────────┘
```

The orchestrator asks the model for tool calls, runs them through the **same**
`ToolRouter` that MCP clients use, and feeds results back until the model
answers.

The browser talks to it over a **stateful, streaming WebSocket** (`/chat/ws`):
the Core owns the conversation, so what the user types reaches the model and the
loop streams back live — the assistant's narration, each tool call and its
result, then the reply. The chat is a real-time **mirror of the agent**, and
because the Core (not an external MCP client) drives the loop, the browser →
model → browser round-trip is just a normal bidirectional socket. A stateless
`POST /chat` (whole transcript in, final reply out) remains for simple callers.

Two surfaces are exposed over it:

- **Frontend panel** — drop `<selvans-chat />` into your Angular app. The chat
  is scoped to that app's tools, so the assistant operates the UI the user is
  looking at, live.
- **Multi-app console** — `http://localhost:8080/ui/console`. One chat, an
  app-switcher across every registered app, and the selected app's UI embedded
  in an iframe beside it.

Configure the provider via env (see `.env.example`) — the Core reaches it over
HTTP, so switching is only a matter of these values:

```bash
AI_PROVIDER=ollama          # ollama | openai | anthropic
OLLAMA_MODEL=qwen2.5:7b-instruct
# OPENAI_API_KEY=…   OPENAI_MODEL=gpt-4o-mini
# ANTHROPIC_API_KEY=…   ANTHROPIC_MODEL=claude-sonnet-4-20250514
```

With `ollama` (the default) no API key is needed — use a model that supports
tool calling. If the selected provider is missing its key, the chat returns a
clear, actionable error (an `error` frame over the socket, or an error body from
`POST /chat`) and the rest of the Core keeps working.

---

## Authentication

App-to-Core authentication is available as an opt-in first step (phase 0-1 of the
[auth design](docs/architecture/auth-and-identity.md)). Apps authenticate to the
Core with a shared-secret token bound to their `appId`, and the Core refuses to let
one app hijack another's live `appId`.

Configured via env on the Core:

```bash
# off (default) | permissive (verify + log, don't block) | enforce (reject)
Selvans_AUTH_MODE=enforce
# per-app shared secrets, keyed by appId
Selvans_APP_TOKENS='{"demo-backend":"change-me"}'
```

Each app then presents its token — `SelvansBeConfig(token=…)` (Python),
`SelvansModule.forRoot({ token: … })` (Angular). With `AUTH_MODE=off` behaviour is
unchanged, so existing deployments are unaffected. Roll out with `permissive` to
observe violations in the logs before switching to `enforce`.

The same `AUTH_MODE` also governs **AI/MCP clients**: each client is given a bearer
token and an allow-list of apps, so it can only see and call the tools it is
authorized for (tool calls are attributed to the real client id, not a shared
constant):

```bash
Selvans_MCP_CLIENTS='{"claude-desktop":{"token":"change-me","allow":["demo-backend"]}}'
```

> In `enforce` mode you must configure **both** `Selvans_APP_TOKENS` and
> `Selvans_MCP_CLIENTS`, otherwise apps / AI clients are rejected.

### End-user identity

The AI can act **on behalf of an end user**: it presents the user's token in the
`X-Selvans-User` header, the Core verifies it (HS256 JWT when `Selvans_USER_JWT_SECRET`
is set; otherwise an opaque, unverified subject id), and propagates the identity to
your app on every call. Every tool call is then attributable to a real user in the
telemetry — closing the "we don't know which user made a request" gap.

```bash
Selvans_USER_JWT_SECRET=change-me   # verify the user JWT (omit for opaque dev mode)
```

Your backend reads the caller without changing operation signatures:

```python
from selvans import operation, current_user_id

@operation("list", description="List the current user's tasks")
async def list_tasks(self) -> list[dict]:
    return await db.list_tasks(owner=current_user_id())   # None if no user propagated
```

Angular receives the same context on the `toolCall$` stream. Identity is propagated
and audited regardless of `AUTH_MODE`; it is about attribution, not access control,
so an absent/invalid user token never blocks a call (it is logged).

---

## License

Distributed under the Apache 2.0 license — see LICENSE and NOTICE.