Metadata-Version: 2.4
Name: seren-workbench
Version: 2.2.0
Summary: MCP server for the Seren stack — the tool surface LLMs reach through.
Author: Chad Roesler
License: AGPL-3.0-or-later
Project-URL: Homepage, https://github.com/ChadRoesler/SerenWorkbench
Project-URL: Repository, https://github.com/ChadRoesler/SerenWorkbench
Project-URL: Bug Tracker, https://github.com/ChadRoesler/SerenWorkbench/issues
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp<2,>=1.3.0
Requires-Dist: fastapi>=0.110
Requires-Dist: uvicorn[standard]>=0.29
Requires-Dist: httpx>=0.28
Requires-Dist: pyyaml>=6.0
Requires-Dist: seren-meninges<3,>=2.4.0
Requires-Dist: seren-sinew<2,>=1.11.0
Provides-Extra: updates
Provides-Extra: corp
Requires-Dist: truststore>=0.9; extra == "corp"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Dynamic: license-file

# SerenWorkbench

The tool surface an LLM reaches through.

One process on port **7425** serving an MCP endpoint, an operator dashboard and a small HTTP
API. It is the one place a model connects: the standard Seren components —
Memory, Loci, the Corpus Callosum, the Hippocampus and Lodestar — are **passed
through**, so the Workbench offers each one's own tools exactly as that service
defines them, with a switch per component (`components:` in the config,
`GET /components` to see what each is offering). Builtins cover web search,
time and the cluster. On top of those, you can add your own tools as **YAML
files** — no Python, no restart.

Part of the [Seren](https://github.com/ChadRoesler) stack, but it doesn't
require the rest of it. Point it at whichever services you actually run.

---

## Install

```bash
pip install seren-workbench
python -m seren_workbench
```

That's a working server with the builtin tools and an empty tool
directory. Configuration is optional — copy `seren-workbench.yaml.sample`
to `seren-workbench.yaml` when you want to change ports, service URLs or
which tools start enabled.

```bash
python -m seren_workbench --config /etc/seren/workbench.yaml --port 7425
```

## Connect a client

The MCP endpoint is at `/mcp/` (trailing slash — a bare `/mcp` gets a 307).

```jsonc
{
  "mcpServers": {
    "seren-workbench": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://127.0.0.1:7425/mcp/",
               "--transport", "http-only"]
    }
  }
}
```

Set a bearer token in the config (or `SEREN_WORKBENCH_BEARER_TOKEN`) before
this leaves localhost - a bind beyond loopback with no token refuses to start
and prints the ways out.

If Memory or Lodestar were installed with a token, tell the builtins what to
present:

```yaml
services:
  bearer_token_env: SEREN_CLUSTER_TOKEN        # one token for memory / lodestar / scheduler
  # memory_bearer_token_env: SEREN_MEMORY_TOKEN  # or per service
```

---

## Adding your own tools

Drop a YAML file in `tools_dir` (default `~/seren-workbench/tools`):

```yaml
schema_version: 1
tools:
  - name: disk_free
    description: >
      Report free space on the Seren data volume. Use when someone asks
      whether there's room for another model.
    invoke:
      kind: process
      argv: ["df", "-h", "--", "{mount_point}"]
    parameters:
      - name: mount_point
        type: string
        required: true
        pattern: "/[A-Za-z0-9._/-]*"
```

Then reload — the tool is live immediately, no restart:

```bash
curl -X POST localhost:7425/tools/manifests/reload
```

Tools can run a **process** or make a **web** call. Parameters are typed,
defaulted, range-checked and — for strings — matched against a `pattern`
or an `enum`, with the rules published into the JSON schema so the model
reads them rather than discovering them by being corrected.

**→ [docs/TOOL-MANIFESTS.md](https://github.com/ChadRoesler/SerenWorkbench/blob/main/SerenWorkbench/docs/TOOL-MANIFESTS.md)**
is the full reference.
**[examples/tools/example-tool.yaml](https://github.com/ChadRoesler/SerenWorkbench/blob/main/SerenWorkbench/examples/tools/example-tool.yaml)**
is an annotated file you can copy; the test suite loads it, so it can't
quietly stop being true.

<!-- Absolute URLs on purpose: this file is the PyPI long_description, and
     PyPI does not resolve relative links. -->


### Tools the model asks for

A model can propose a tool for itself with `propose_tool`. The manifest lands
in a staging directory that **nothing loads** — it's inert text until you read
it and approve, at which point it moves into `tools/` and goes live.

Open `/viewer` → **Proposals**; the tab shows a count when something's waiting.
Or from the shell:

```bash
curl -s localhost:7425/proposals                     # what's waiting
curl -s localhost:7425/proposals/prop_a1b2c3d4e5     # the manifest + what it would run
curl -X POST localhost:7425/proposals/prop_a1b2c3d4e5/approve
curl -X POST localhost:7425/proposals/prop_a1b2c3d4e5/reject \
     -d '{"critique":"argv shells out to a script I cannot read"}'
```

**Approving installs it switched off.** The tool appears in your list,
disabled; you turn it on separately from the Tool State tab. Two decisions —
"this isn't malicious" and "I want it live now" — kept apart on purpose.

The model can propose and read its proposals. It cannot approve — there's no
MCP tool behind any approval route, and a test asserts there never is. Reject
requires a critique, which the proposer reads and can revise against; same
loop as the memory consolidator's draft gate.

Turn it off with `dashboard.proposals_enabled: false`, which removes the tools
entirely rather than leaving them to fail.

A model can ask for a whole **MCP server** the same way, with `propose_plugin`:
an address and a reason. Nothing is dialled until you approve; then the server
is plugged in and every tool it offers arrives switched off, for you to turn on
one at a time. A proposal never carries a token — it can only name where one is
kept on this box, and the review says in so many words that approving sends it.

**→ [docs/TOOL-PROPOSALS.md](https://github.com/ChadRoesler/SerenWorkbench/blob/main/SerenWorkbench/docs/TOOL-PROPOSALS.md)** — what to look at when reviewing one.

### What reload won't do

It reads files that are already on disk. It cannot author a tool, it
refuses a manifest that would shadow a builtin, it won't re-enable
something you switched off, and it treats a *missing* tools directory as
"something's wrong" rather than "delete everything." A tool exists because
a person put it somewhere.

Neither does a restart re-enable anything: toggles are remembered in
`<tools_dir>/.tool-state.json`. A remote `from:` import may only hand you
`kind: web` tools, and no tool may point back at the Workbench itself.

---

## What a connected model can count on

- **The list it has is the list there is.** The Workbench tells connected
  clients when its tools change (`notifications/tools/list_changed`): a
  component that came up late, a switch you flipped, a proposal you approved.
  A tool that is switched off is not offered at all, and appears the moment
  it is switched on.
- **A failure is an error.** A builtin that could not do what was asked comes
  back as an MCP error, not as a successful result whose text is bad news.
- **It can read its own record.** `list_my_tool_calls` is the audit log from
  the caller's side: which tools, when, whether they worked. Names and
  outcomes only — never an argument or a result.
- **A service it asks for stays up while it is using it.**
  `ensure_service_running` takes a lease with Lodestar and `release_service`
  gives it back; the service is stopped when its last holder lets go. A hold
  a session forgot is let go by the Workbench (`tools.ensure_service_running.
  hold_minutes`, and at shutdown). `GET /components` shows what is held.

## Snapshots

The tools folder — manifests, approved plugins, proposals, and the switches —
is what a Workbench keeps, and it is snapshotted like the rest of the Seren
family: daily by default, into `backups/` beside the tools folder.

```bash
curl -s localhost:7425/stores                                  # what is kept, and the snapshots
curl -X POST localhost:7425/stores/snapshot -d '{"reason":"before I tidy up"}'
curl -X POST localhost:7425/stores/snapshots/<id>/rehearse     # a restore's dry run
```

Putting one back is two config keys and a restart, into an empty tools folder
only — there is no route and no tool that restores:

```yaml
backup:
  restore_from: /path/to/backups/seren-workbench/20261006-151256
  restore_reason: "moving to the new box"
```

---

## Endpoints

| | |
|---|---|
| `GET /` | service info, tool counts and update status |
| `GET /health` | liveness |
| `GET /tools` | every tool with its schema |
| `GET`/`POST` `/tools/state` | enable and disable, per tool or per action |
| `GET /tools/manifests` | what loaded, what was skipped, and why |
| `POST /tools/manifests/reload` | re-read the directory, apply it live |
| `GET /components` | each component and plugin: on or off, reachable or why not; what is held |
| `POST /components/state` | switch a component or plugin on or off |
| `GET /proposals` | tools and plugins the model has asked for |
| `GET /proposals/{id}` | one, with the full manifest and what it would run |
| `POST /proposals/{id}/approve` | install it, switched off |
| `POST /proposals/{id}/reject` | refuse it, with a critique |
| `GET /config` | resolved config, secrets masked |
| `GET /logs` | recent server logs |
| `GET /stores` | what the Workbench keeps, and its snapshots |
| `POST /stores/snapshot` | take a snapshot now |
| `POST /stores/snapshots/{id}/rehearse` | prove one can be put back, without putting it back |
| `/mcp/` | the MCP streamable-HTTP transport |
| `/viewer` | the operator dashboard |

## Update checking

`GET /` reports whether a newer `seren-workbench` has been published, alongside
the tool counts:

```jsonc
{
  "service": "SerenWorkbench",
  "version": "1.2.0",
  "tools_count": 26,
  "updates": {
    "status": "ok",
    "installed": "1.2.0",
    "latest": "1.3.0",
    "update_available": true
  }
}
```

On by default: update checking is part of the core install (it lives in
`seren-meninges`). A box that never leaves the LAN has no business calling
PyPI; set `enabled: false` there. The knobs:

```yaml
updates:
  enabled: true
  check_interval_hours: 6      # cached; never checked per-request
  allow_prerelease: false
```

The result is cached and the check never happens in the request path, so `/`
stays fast. `updates.status` is always one of `ok`, `disabled`, `unavailable`
(a broken install: the checker's dependencies are missing) or `error`
— **never absent, and never a silent "you're fine" when it couldn't actually
check.**
`SEREN_WORKBENCH_UPDATES_ENABLED=false` turns it off without editing config.

**The Workbench never upgrades itself.** Applying an update is `pip install -U
seren-workbench` and a restart — your service supervisor's job, not this
process's. A running process can't reliably swap its own code out from under
itself, so it doesn't pretend to.

## Development

```bash
pip install -e ".[dev]"
pytest -q
```

## License

AGPL-3.0-or-later.
