Metadata-Version: 2.4
Name: etna-mcp
Version: 1.0.0b48
Summary: A lightweight tool server for AI models — extend with drop-in kit files.
Author: dwhite-sys
License: Apache-2.0
Project-URL: Homepage, https://github.com/dwhite-sys/Etna
Project-URL: Repository, https://github.com/dwhite-sys/Etna
Project-URL: Issues, https://github.com/dwhite-sys/Etna/issues
Keywords: mcp,model context protocol,llm,tools,kit,claude,openwebui,lm studio,etna,ai
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: fastapi>=0.111.0
Requires-Dist: uvicorn[standard]>=0.29.0
Requires-Dist: requests>=2.31.0
Requires-Dist: uv>=0.4.0
Requires-Dist: packaging>=24.0

# Etna

A tool server built for agentic harnesses. Etna gives you tool isolation out of the box — each kit is its own branch of the tool tree, independently toggleable per session. Drop a Python file in, and it's live.

Kit authoring is intentionally Pythonic: a decorator, type hints, and a docstring are all the model needs to understand and use a tool. Everything else — schema generation, dependency management, client registration, hot-reload — is handled for you.

## Why Etna instead of MCP

MCP solves the problem of getting tools into models, and it does that well. What it doesn't solve is what happens when you have a lot of them. The standard answer is to run a separate server process per tool category and register each one individually in your client config. That works, but it means your tool surface area is defined at the process level — adding a capability requires restarting something, and toggling access requires editing a config file.

Etna takes a different position. One server, one connection, all your kits. Isolation happens at the kit level inside that single process — each kit is its own independently toggleable branch of the tool tree. A harness can expose different kits to different models, different sessions, or different users, all from the same running server. No additional processes, no config restarts.

MCP compatibility ships as an adapter so Etna works with any client that already speaks MCP. But the Etna Protocol is its own thing — built directly on LLM tool use, not on top of MCP.

## How it works

- **CLI (`etna`)** — manages the full lifecycle. Install kits, start the server, configure clients, manage Chrome for browser automation. One command for everything.
- **Kits (`~/.etna_server/kits/`)** — each kit is a plain Python file with `@tool`-decorated functions and four metadata fields. Drop one in, and it's live.
- **Server** — FastAPI process that auto-discovers all kits at startup and again on hot-reload. Speaks both the Etna Protocol and MCP JSON-RPC on the same port.
- **Etna Protocol** — a tree-based tool discovery protocol in the same category as MCP, not derived from it. Built directly on LLM tool use. The server is the root. Kits are branches. Tools are leaves. Clients walk the tree, toggle branches on and off per session, and search across tools by keyword. MCP compatibility is an adapter — it's how Etna connects to clients that already speak MCP, not the foundation the protocol is built on.
- **Skills** — markdown instruction documents paired with kits or installed standalone. A harness can discover and load skills into context, giving the model guidance on how to use a kit's tools or how to approach a class of tasks.
- **Registry (`etna/utils/registry.py`)** — the `@tool` decorator registers functions into a global dict at import time. Type hints are scraped to build JSON Schema automatically. Docstrings become the tool description shown to the model — write them for the model, not for humans.
- **Config (`~/.etna_server/`)** — kit installs, port assignments, client registrations, and per-kit config values all live here. Written atomically, read at runtime.

## Kit anatomy

```python
# my_kit.py
from utils import tool
import os

kit_name        = "My Kit"
kit_description = "What this kit does."
requirements    = ["requests"]
config          = {"MY_VAR": "default_value"}

@tool
def do_something(query: str, limit: int = 10) -> dict:
    """
    WHEN TO USE: one sentence describing the task this solves.

    query: what to search for.
    limit: max results (default 10).

    Returns {"results": list, "count": int}.
    """
    my_var = os.getenv("MY_VAR", "default_value")
    return {"results": [], "count": 0}
```

- `requirements` is AST-parsed before the file is ever imported — no import needed, missing packages are installed automatically into Etna's managed venv.
- `config` is a dict of `{"VAR": "default"}` pairs. Values are stored in `~/.etna_server/kit_configs/<kit_stem>/config.json` and exposed as environment variables at runtime. User-set values are preserved across reinstalls.
- `@tool` is the only import required. Type hints are mandatory — they build the JSON Schema. Parameters without defaults are required, parameters with defaults are optional.

## .ekp packages

`.ekp` files bundle a kit and its skill documentation together in a single archive. The skill is installed alongside the kit and automatically surfaced to the model when it inspects that kit.

```
etna install my_package.ekp
```

An `.ekp` is a zip archive with this structure:

```
my_kit.py          ← the kit file (required)
skill/             ← skill folder (optional)
  SKILL.md         ← instructions for the model
  references/      ← reference docs, loaded on demand
  scripts/         ← helper scripts
  assets/          ← any other files
```

## Setup

```
pip install etna-mcp
etna init
```

`etna init` is idempotent: it creates or repairs Etna's managed runtime, installs Etna's per-user startup service, starts it immediately, and verifies the server is healthy. Re-running it after an upgrade refreshes the managed runtime and service definition.

`etna install` with no arguments remains a compatibility alias for `etna init`. UV is installed automatically as an Etna dependency and is invoked through Etna's own Python interpreter, so it does not need to be separately present on PATH.

## Adding a kit

```
etna install my_kit.py
etna install my_package.ekp
```

If the server is already running, the kit is linted, installed, and hot-reloaded immediately — no restart needed.

To install from the curated kit repo:

```
etna install ntfy
etna install ntfy==1.0.0b1
```

Vulcan can also be installed directly through Etna:

```
etna install vulcan
```

This runs Vulcan's official installer for the current operating system. The desktop application then handles its normal graphical setup and runtime repair.

## CLI reference

```
etna init                             Initialize/repair Etna runtime + startup service
etna install                          Legacy alias for etna init when no target is given
etna install <path/kit.py>            Install a kit from a local file
etna install <path/pkg.ekp>           Install a kit package (kit + skill)
etna install <kit_name>               Install a kit from the curated repo
etna install <kit_name>==<version>    Install a specific version from the repo
etna install vulcan                    Install the Vulcan desktop app
etna update <path/kit.py>            Update a kit (no prompt)
etna update <kit_name>               Update a kit from the repo (no prompt)
etna update --all                    Update all installed kits from the repo
etna remove <kit_name>               Remove a kit
etna list                            List installed kits
etna search <query>                  Search the curated kit repo
etna status                          Server, browser, kits, and client summary

etna start                           Start the server
etna start --verbose                 Start with full log output
etna start stdio [kit_name]          Start stdio shim — scopes to /mcp/<kit_name> if given
etna stop                            Stop the server
etna restart                         Restart the server

etna compat                          Auto-detect and configure all clients
etna compat chatgpt                  Create one ChatGPT Desktop plugin per kit
etna compat claude                   Write kit entries to Claude Desktop config
etna compat lmstudio                 Write kit entries to LM Studio config
etna compat cursor                   Write kit entries to Cursor config
etna compat windsurf                 Write kit entries to Windsurf config
etna compat vscode                   Write kit entries to VS Code settings
etna compat continue                 Write kit entries to Continue config
etna compat openwebui <url> <key>    Register kits with OpenWebUI

etna config list <kit>               List config variables for a kit
etna config get <kit> <var>          Get a config value
etna config set <kit> <var> <val>    Set a config value
etna config reset <kit> <var>        Reset a config value to its default

etna browser start                   Launch Chrome with CDP for browser automation
etna browser stop                    Stop Chrome
etna browser status                  Show Chrome CDP status
```

## Etna Protocol endpoints

| Endpoint             | Method | Description                                         |
|----------------------|--------|-----------------------------------------------------|
| `/list_kits`         | GET    | All installed kit names and descriptions            |
| `/inspect_kit`       | POST   | Kit metadata — name, description, skill             |
| `/list_tools_in_kit` | POST   | Full tool schemas for a kit                         |
| `/inspect_tool`      | POST   | Schema for a single tool                            |
| `/run_tool`          | POST   | Execute a tool, returns result directly             |
| `/reload_kit`        | POST   | Hot-reload a kit without restart                    |
| `/unload_kit`        | POST   | Remove a kit's tools from the registry              |
| `/search_tools`      | POST   | Keyword search across all tool names and docs       |
| `/list_skills`       | GET    | All installed general skills (name + description)   |
| `/read_skill`        | POST   | SKILL.md body for any skill — kit or general        |
| `/mcp`               | POST   | MCP JSON-RPC 2.0 — all kits                         |
| `/mcp`               | GET    | SSE keepalive stream — all kits                     |
| `/mcp/<kit_stem>`    | POST   | MCP JSON-RPC 2.0 — scoped to one kit                |
| `/mcp/<kit_stem>`    | GET    | SSE keepalive stream — scoped to one kit            |

### inspect_kit response

```json
{
  "kit_name": "Ntfy",
  "kit_description": "Send push notifications via ntfy.",
  "filename": "ntfy.py",
  "enabled": true,
  "skill": "ntfy"
}
```

`skill` is the skill name if a skill is installed for this kit, `null` otherwise. Use `read_skill` to retrieve the skill content.

### read_skill request/response

```json
{ "skill": "ntfy" }
```

```json
{ "name": "ntfy", "body": "..." }
```

Works for both kit skills (discovered via `inspect_kit`) and general skills (discovered via `list_skills`).

## Skills

Skills are markdown instruction documents that tell the model how to use a kit's tools or how to approach a class of tasks. They live in:

- `~/.etna_server/kit_skills/<kit_stem>/` — paired with a specific kit, surfaced via `inspect_kit`
- `~/.etna_server/skills/<skill_name>/` — standalone general skills, surfaced via `list_skills`

A skill folder contains at minimum a `SKILL.md` with YAML frontmatter:

```markdown
---
name: My Skill
description: What this skill teaches the model.
---

When using this kit, always...
```

Subdirectories (`references/`, `scripts/`, `assets/`) are optional and available for the model to traverse on demand.

## License

Apache 2.0

### ChatGPT Desktop local plugins

Run `etna compat chatgpt` to expose each installed kit as a separate personal
plugin. Packages contain portable `plugin.json` and `mcp.json` files under
`~/.codex/plugins/`; the catalog is `~/.agents/plugins/marketplace.json`. Each
MCP server uses a package-contained launcher for `etna start stdio <kit_stem>`.
The launcher records the resolved Etna executable path when available, while
the portable manifest uses the required contained `./` command. Etna must
already be running.

Kit stems, rather than display names, determine plugin identity. A short hash
in the package name prevents collisions between stems with similar spelling.
Etna ownership markers identify packages for updates and stale removal.
Unrelated catalog entries and plugin folders are preserved; malformed catalogs
or collisions stop configuration rather than overwrite user data. After
registration, kit install/update/remove automatically refreshes this catalog.

Restart ChatGPT Desktop and refresh/install the plugins from the personal
marketplace after configuration or changes. Entries request
`INSTALLED_BY_DEFAULT`; actual loading depends on the host's plugin settings.
Etna updates source packages, not ChatGPT's installed plugin cache. No HTTP
conversion or developer-mode remote server registration is needed for these
local packages. See the [OpenAI local plugin packaging documentation](https://developers.openai.com/plugins/build/plugins).

`etna compat` detects existing configuration directories for Claude Desktop,
LM Studio, Cursor, Windsurf, VS Code, and Continue, plus `~/.codex` for the
shared ChatGPT Desktop/Codex plugin setup. It offers to configure all detected
clients or select them individually, using the same writers as explicit
`etna compat <client>` commands. Detection is based on configuration directories,
not an executable scan; use the explicit command for a client that has not yet
created its directory. OpenWebUI requires an explicit URL and API key.

## Windows 11 diagnostics (1.0.0b48)

Upgrade using the same Python installation that runs Etna, then repair the runtime:

```powershell
py -m pip install --upgrade --pre etna-mcp==1.0.0b48
py -m etna --version
py -m etna init
py -m etna status
py -m etna logs
```

If you installed with uv instead of pip, use `uv tool upgrade etna-mcp`, then
`etna init`. Etna runs natively on Windows; WSL is not required. Its scheduled task
starts at your user login, uses the managed interpreter, and preserves the config
location used during initialization. Existing generated files with Windows-1252
encoding are repaired without deleting kits or their configuration.

`etna logs` includes version, Python, OS, config directory, and the last 100 lines
of each retained log. Background startup writes `server.log`; the Windows
scheduled task writes `service.log`. The default location is `%APPDATA%\Etna`
(or `ETNA_CONFIG_DIR` if overridden). For a failure before server launch, capture
initialization output too:

```powershell
py -m etna init *> etna-init.log
py -m etna logs > etna-logs.txt
```

Send those files with the error screenshot. After initialization works, test
stop/start/restart, close the terminal while the server is running, and sign out
and back in to verify login startup. Then test kit installation, one tool call,
client connection, and `etna browser start` / `etna browser stop`.

Release CI exercises Windows and Linux on Python 3.11, 3.12, and 3.14, including
legacy encoding repair, paths with spaces and Unicode, actual managed runtime
startup/shutdown, health checks, and an MCP tool call. Scheduled login and desktop
client/browser behavior still require testing on a Windows 11 device.

### Windows server exits after startup (1.0.0b48)

Windows background launches use a windowless process group. A supervisor records
child startup, runtime, decimal/hexadecimal exit codes, and up to three restart
attempts in `lifecycle.log`. It forwards real log file handles to the server,
including when launched through `pythonw.exe`; unbuffered output and Python's
fault handler preserve crash details when possible. Ordinary server startup and
shutdown messages are enabled, and logs are appended rather than erased.

`etna logs` now includes `lifecycle.log` and Windows' verbose scheduled-task status.
An external force-kill of the entire process tree cannot write its own final log;
the last lifecycle event and task result remain useful evidence. Explicit
`etna stop` records the request and terminates the Windows process tree together
so the supervisor does not restart a deliberately stopped server.

Release smoke tests now verify health for 20 seconds after the starting CLI exits,
then perform a tool call. This checks for the reported brief-online-then-offline
failure, while the Windows 11 device test still determines whether it persists
on the user's machine.
