Metadata-Version: 2.5
Name: persona-mcp-workspace
Version: 0.2.0
Summary: Per-job shared workspace (files) exposed as MCP tools for persona agents and the workflow engine
Requires-Python: >=3.10
Requires-Dist: fastmcp>=2.8.0
Requires-Dist: pillow>=12.3.0
Requires-Dist: python-dotenv>=1.0.0
Description-Content-Type: text/markdown

> # ⚠️ Questo repository è stato migrato
>
> Lo sviluppo continua su GitHub:
> **https://github.com/applica-software-guru/persona-mcp-workspace**
>
> Questa copia su Bitbucket è **congelata** e non riceve più commit,
> release o pipeline. Aggiorna i tuoi remote:
>
> ```bash
> git remote set-url origin git@github.com:applica-software-guru/persona-mcp-workspace.git
> ```
>
> Il pacchetto resta su PyPI (`persona-mcp-workspace`); l'immagine Docker
> è su `ghcr.io/applica-software-guru/persona-mcp-workspace`.

# persona-mcp-workspace

Full-machine access exposed as MCP tools: `read`, `write`, `edit`, `bash`.
This server is meant to be installed on a real machine so an agent can
operate it remotely — the remote equivalent of a local coding agent's own
tools, with no folder confinement. Paths are used exactly as given
(absolute, or resolved against the server process's own working
directory); `bash` runs with no forced cwd, inheriting the server
process's own.

## Tools

| tool | description |
|---|---|
| `read` | read a file — text with offset/limit, or images returned as image content a vision model can see |
| `write` | create or overwrite a file, parents auto-created |
| `edit` | precise text replacement — one or more disjoint edits per call |
| `bash` | run a shell command, no working directory imposed |
| `skill` | load one skill's full instructions + bundled file paths |

Listing, searching, deleting and moving files are **not** separate
tools — `bash` covers them (`ls`, `grep`, `find`, `rm`, `mv`). One fewer
surface to keep in sync with the agent-facing API, and the agent already
knows how to use a shell.

## Images

`read` detects images by magic bytes (never by extension) and returns
them as MCP image content — `[text note, image]` — so a vision-capable
model sees the actual image instead of base64 text. Supported: jpg,
png, gif, webp inline; bmp (and anything Pillow can decode) converted to
png; animated PNGs rejected. Images already within 2000x2000 and 4.5MB
of base64 pass through byte-for-byte; larger ones are resized (EXIF
orientation applied) with a note telling the model how to map displayed
coordinates back to the original. Pass `binary_base64=true` to get the
raw bytes without processing. Limits are env-tunable:
`WORKSPACE_IMAGE_MAX_DIMENSION`, `WORKSPACE_IMAGE_MAX_ENCODED_BYTES`,
`WORKSPACE_MAX_IMAGE_READ_BYTES`.

There is no dedicated node in persona-workflows for these tools: a
workflow mounts this server on an agent step (or calls it) through the
existing generic `tool` node, which is agnostic to whatever tools a given
MCP server exposes — it never needs updating when this server's tool
surface changes.

## Skills

Skills are installed on disk under `.agents/skills/<name>/SKILL.md`
(override the root with `WORKSPACE_SKILLS_DIR`) — the same on-disk Agent
Skills convention used by Claude Code and other harnesses: YAML
frontmatter (`name`, `description`, optional `allowed-tools`) followed by
the skill's instructions body, plus optional `scripts/`, `references/`,
`assets/` subfolders. Drop a new skill folder in and it's picked up
immediately, no restart needed.

The catalog (name, description and SKILL.md location of every installed
skill) is exposed as
the `resource://skills` MCP resource, not a tool — see below for why
and how to wire it into the system prompt. `skill(name)` is the one
remaining tool: it loads one skill's full instructions plus the relative
paths of its bundled files — fetch those with `read`, same reasoning as
above (no separate content-fetch tool).

## MCP resources

| resource | returns |
|---|---|
| `resource://cwd` | this server's working directory |
| `resource://skills` | the skill catalog — one `<skill>` element (name, description, location) per installed skill, wrapped in `<available_skills>` |
| `resource://system` | OS/Python version and which shell `bash` runs (bash vs PowerShell) |

`resource://cwd` reflects `WORKSPACE_CWD` if set at startup (the process
`chdir`s there before doing anything else, so `read`/`write`/`edit`
relative paths and `bash`'s cwd all agree with what's reported);
otherwise it's whatever directory the process was launched from.

### Getting these into the system prompt

**MCP resources are not read automatically by persona-core** — there's
no handshake-time mechanism that pulls them in for you. To wire one in,
configure a resource-backed `Variable` on the agent:

```json
{
  "name": "cwd",
  "source": {
    "type": "resource",
    "mcpServer": "<this feature's registered name>",
    "resourceUri": "resource://cwd"
  }
}
```

(same shape for `resourceUri: "resource://skills"`) and reference
`{{ cwd }}` / `{{ skills }}` inside the agent's `systemInstructions`.
persona-core resolves each fresh on every turn (no caching), so a skill
added after the agent was created still shows up. Without this wiring,
`skill(name)` still works as an explicit tool call — you'd just be
missing the ambient catalog telling the agent what's available to load.

### `edit` semantics

`edit(path, edits: [{old_string, new_string, replace_all?}])`. Each
`old_string` must match exactly once in the file unless `replace_all` is
set, in which case every occurrence is replaced. Edits in one call are
applied in order against the progressively-edited content. All edits are
validated before anything is written — if one fails (not found, or
ambiguous without `replace_all`), the file is left untouched.

## No confinement, by design

There is no per-job workspace folder and no path jail: `read`/`write`/
`edit` accept any path this process can reach, and `bash` has no forced
working directory. Running this server means trusting the project's
agents with the machine under the server's user account — the **only**
gate is authentication (JWT), not a filesystem boundary. Install it under
a dedicated user with exactly the permissions you want to grant.

`project_id` (from the verified JWT) and `execution_id`/`workspace_id`
(when the caller sends them in the MCP request `_meta` — persona-core
forwards the `AgentContext` there automatically, persona-workflows sets
`workspace_id` to the root execution id) are logged with every call for
audit/traceability, but they never restrict what a call can touch.

`bash` shells out through each host's native shell: `bash` on POSIX,
Windows PowerShell (`powershell.exe`, built in, no Git Bash/WSL needed)
on Windows. Command syntax has to match the host, so wire
`resource://system` into the agent's system prompt the same way as
`resource://cwd` if it may run against either OS.

`WORKSPACE_SHELL_MAX_TIMEOUT` (default 600s) caps per-command timeouts.
`WORKSPACE_CWD`, if set, is `chdir`'d into at startup, before anything
else — otherwise the process just keeps whatever cwd it was launched
with. `WORKSPACE_SKILLS_DIR` (default `.agents/skills`) points at the
skills root.

## Running

```bash
uv sync
uv run persona-mcp-workspace --port 9100    # JWT auth on (default)
uv run persona-mcp-workspace --no-auth      # local development

# or, from any machine with no local checkout:
uvx persona-mcp-workspace --port 9100
```

Register it in persona as a global feature template (type `mcp`,
transport `http`, `enablePersonaAuth: true`) named `workspace`; mount it
on an agent step via its per-step `toolsets`, or invoke it directly with
a generic `tool` node.

### Docker

```bash
docker run -p 9100:9100 \
  -v persona-workspace:/workspace \
  ghcr.io/applica-software-guru/persona-mcp-workspace:latest
```

The image starts the server on `0.0.0.0:9100` with JWT auth on, and
`WORKSPACE_CWD=/workspace` — mount a volume there to keep files across
restarts. Append flags to override (`--no-auth`, `--port`, …) and pass
`-e JWKS_URI=… -e JWT_ISSUER=… -e JWT_AUDIENCE=…` to point the verifier
at your own persona instance instead of the defaults.

The container runs as root and `bash` executes whatever the agent sends,
so it is as unconfined as the rest of this server — the container is the
boundary. Don't mount host paths you aren't willing to hand over, and
don't expose the port publicly with `--no-auth`.

#### The `-playwright` variant

`:<version>-playwright` / `:latest-playwright` is the same server plus
browser automation: Node, [`@playwright/cli`][pwcli] and **Google Chrome**
(the branded stable build, not bundled Chromium — sites that sniff the
browser behave as they do for real users), with the `playwright-cli`
skill already installed, so `skill` lists it and `bash` can drive a
browser out of the box.

```bash
docker run -p 9100:9100 -v persona-workspace:/workspace \
  ghcr.io/applica-software-guru/persona-mcp-workspace:latest-playwright
```

[pwcli]: https://www.npmjs.com/package/@playwright/cli

Because `/workspace` is a volume mount point, anything written there at
build time would vanish under a mount. The initialized workspace is
therefore baked into `/opt/persona/seed` and copied in by the entrypoint
at startup, **never overwriting what is already there** — an existing
volume keeps the agent's own files and its edited skills. Chromium lives
in `/opt/ms-playwright`, outside both the volume and `$HOME`.

The seeded `.playwright/cli.config.json` pins `channel: chrome` and sets
`chromiumSandbox: false`: branded Chrome refuses to start as root, which
is what every process in this container is. Delete the file from the
volume (or edit it) to choose differently.

amd64 only — Google Chrome ships no arm64 Linux build — and ~1.5GB
against the base image's ~344MB.

## Releasing

Push a bare semver tag and GitHub Actions does the rest — tests, then
the PyPI publish and the multi-arch (amd64/arm64) image push to GHCR in
parallel:

```bash
git tag -a 0.2.0 -m 0.2.0 && git push origin 0.2.0
```

The tag name *is* the version: it gets stamped into `pyproject.toml` for
both the wheel and the image, which is published as
`ghcr.io/applica-software-guru/persona-mcp-workspace:0.2.0` and
`:latest`.

No publishing secrets are stored in the repo: PyPI uses trusted
publishing (OIDC, configured on the PyPI project against this repo and
`release.yml`) and GHCR authenticates with the built-in `GITHUB_TOKEN`.
