selvans
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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing