Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
23db777
feat(mcp): add osw-mcp server exposing a live OSL instance
LukasGold Jul 16, 2026
e219aae
feat(mcp): port server to mcp 2.x and isolate the extra
LukasGold Aug 22, 2026
a7c35a7
feat(mcp): authenticate from an osw credential file
LukasGold Aug 22, 2026
d393a66
feat(mcp): select between multiple OSL instances at runtime
LukasGold Aug 22, 2026
ec54bcd
refactor: extract SDK-free osw.service core from osw.mcp
LukasGold Aug 25, 2026
431f33f
refactor(service): lift search tools into osw.service.ops
LukasGold Aug 25, 2026
1959876
refactor(service): lift remaining tool bodies into osw.service.ops
LukasGold Aug 25, 2026
c1cede3
feat(cli): add typer CLI assembled from the operation registry
LukasGold Aug 25, 2026
4ed8c7c
feat: remove filesystem paths from the MCP surface
LukasGold Aug 25, 2026
cb7d807
feat(mcp): pin one instance per server, wire annotations and meta
LukasGold Aug 25, 2026
b6fc0c0
refactor(mcp): drop tool closures, fold registration into server
LukasGold Aug 26, 2026
c3ee9de
build: type-check src/osw/mcp instead of excluding it
LukasGold Aug 26, 2026
cf21923
docs: document the osw CLI and unify the config reference
LukasGold Aug 26, 2026
085db4a
fix(config): find .env from the CWD, report config sources
LukasGold Aug 26, 2026
1070646
docs: move CLI and MCP sections out of the README
LukasGold Aug 27, 2026
a1ef49a
docs: default MCP examples to a credential file in env
LukasGold Aug 27, 2026
cd17247
feat(mcp): require an explicitly configured OSW_DOMAIN
LukasGold Aug 27, 2026
3d91d92
docs: restructure the CLI and MCP guide
LukasGold Aug 28, 2026
3a41c48
docs: simplify the CLI and MCP setup section
LukasGold Aug 28, 2026
84b69e9
added local folders to .gitignore
LukasGold Aug 28, 2026
951a66e
docs: correct why the CLI may infer an instance
LukasGold Aug 28, 2026
c1ef65e
Merge origin/main into feat/mcp-server
LukasGold Aug 28, 2026
8f81fec
refactor: de-isolate the mcp extra from the dev environment
LukasGold Aug 28, 2026
be75e47
docs: drop remaining references to the separate MCP environment
LukasGold Aug 28, 2026
64d99d6
fix: allow uploading a file from an in-memory stream
LukasGold Aug 28, 2026
2051c26
refactor(service): validate Settings with pydantic
LukasGold Aug 28, 2026
4a933af
Merge fix/in-memory-file-upload into feat/mcp-server
LukasGold Aug 28, 2026
4b7dd7e
test: stop test_init_from_env_vars leaking OSW_CRED_FILEPATH
LukasGold Aug 28, 2026
f4ef72c
test: do not assume the first ask-query hit carries jsondata
LukasGold Aug 28, 2026
56711ed
fix(service): validate read_only via pydantic instead of truthy set
LukasGold Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,8 @@ playground
/osw_files/
*/accounts.pwd.yaml
/accounts.pwd.yaml
.ign
.claude

# Local folders
.ign/
.claude/
graphify-out/
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pip install osw
```

Optional extras (`osw[wikitext]`, `osw[DB]`, `osw[S3]`, `osw[dataimport]`,
`osw[UI]`, `osw[all]`) are described in the
`osw[UI]`, `osw[mcp]`, `osw[all]`) are described in the
[Get Started guide](https://opensemanticlab.github.io/osw-python/get-started/).

## Quickstart
Expand All @@ -39,6 +39,19 @@ More runnable scripts live in [examples/](examples/), and the
[Basics tutorial](docs/tutorials/basics.ipynb) walks through the
OpenSemanticLab data model.

## CLI and MCP tools

Installing `osw` also installs an `osw` command line client, and the
`osw[mcp]` extra adds an MCP server that exposes a live instance to agent
clients such as Claude Code:

```bash
osw search ask '[[Category:Item]]' --limit 5
```

Commands, tools and their configuration are described in the
[CLI and MCP guide](https://opensemanticlab.github.io/osw-python/cli-and-mcp/).

## Contributing

Contributions are welcome, see [CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
339 changes: 339 additions & 0 deletions docs/cli-and-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
# CLI and MCP tools

Besides the Python API, osw ships two adapters that talk to a live instance:
the `osw` command line client, and an MCP server for agent clients such as
Claude Code. Both run the same operations from one shared, SDK-free core
(`osw.service`), so a command and its matching tool behave identically. They
differ in exactly one way: only the CLI accepts filesystem paths.

## Setup

Install one of the two; the second includes the first:

```bash
uv tool install osw # the `osw` command
uv tool install "osw[mcp]" # the same, plus the `osw-mcp` server
```

<details markdown="1">
<summary>Other ways to install</summary>

```bash
pip install "osw[mcp]" # into the active environment
uv add "osw[mcp]" # as a dependency of the current uv project
uvx --from "osw[mcp]" osw-mcp # run the server without installing it
```

`uvx` is what the registration examples further down use, so the server needs
no install of its own.

</details>

`osw[mcp]` is also part of `osw[all]`. The other extras are listed in the
[Get Started guide](get-started.md#optional-extras).

Both adapters need an instance and credentials. The quickest start is a
gitignored `.env` file in your project root:

```dotenv
OSW_DOMAIN=wiki-dev.open-semantic-lab.org
OSW_USERNAME=your-user
OSW_PASSWORD=your-password
```

The CLI finds that file by searching upward from the working directory, so
`osw status` now reports the instance and connection state. The MCP server does
not search: its settings come from the `env` block of its registration, see
[Registering a server](#registering-a-server). Everything that can be set is
listed under [Configuration](#configuration).

## Command line

```bash
osw status
osw search ask '[[Category:Item]]' --limit 5
osw entity get 'Item:OSW1234...' --json | jq .
osw file cat 'File:Example.csv' # inline text
osw file download 'File:Example.csv' --target-dir ./tmp # to disk
```

Commands are grouped by subject:

| Group | Commands |
| --- | --- |
| `entity` | `get`, `put`, `export`, `delete` |
| `file` | `info`, `cat`, `write`, `download`, `upload` |
| `search` | `ask`, `text`, `instances`, `sparql` |
| `slot` | `list`, `get`, `set` |
| `schema` | `get` |
| `instance` | `list` |
| `ledger` | `path` |
| top level | `status` |

Global options apply to every command:

- `--instance IRI` picks the instance. Optional: it is only required when
`OSW_DOMAIN` is not set and the configured credential file holds more than
one iri.
- `--json` / `-j` writes machine-readable JSON to stdout and keeps osw's own
progress output on stderr, so it pipes cleanly into `jq`.
- `--read-only` refuses write operations.
- `--verbose` / `-v` shows full tracebacks instead of a one-line message.

Failures exit non-zero with a short message on stderr and no traceback.

## MCP server

`osw[mcp]` ships an [MCP](https://modelcontextprotocol.io) server that exposes a
live OpenSemanticLab instance to MCP clients such as Claude Code. It wraps
`OswExpress` and provides tools to search (semantic / SPARQL / full-text),
introspect category schemas, read entities and every page slot, create/update
and delete entities, and read and write file pages as text. The transport is
stdio; SSE and HTTP are not supported.

**No filesystem access:** no MCP tool takes or returns a local path. File
content moves inline as text (`get_file_info`, `read_file_text`,
`write_file_text`), and everything path-based lives in the CLI instead
(`osw file download`, `osw file upload`, `osw ledger path`).

**One server per instance:** each server process is pinned to exactly one OSL
instance for its whole lifetime; there is no tool to switch at runtime.
`OSW_DOMAIN` must be set, either in the server entry's `env` block or in the
`.env` file that entry names. Without it the server refuses to start rather than
register tools that would all fail.

### Registering a server

A server entry can carry its settings in two ways:

- **Directly in the entry's `env` block.** Every variable from the
[reference table](#variable-reference) can be set there, so no `.env` file is
needed at all.
- **In a `.env` file**, named by `OSW_ENV_FILE` in the `env` block. Useful when
several tools share one settings file, or when the client config is committed
and the settings file is not.

Prefer the `env` block naming `OSW_CRED_FILEPATH` and `OSW_DOMAIN`: the
destination instance is spelled out in the entry itself, so it is visible at a
glance and in a diff rather than one indirection away. The secret stays out of
the client config either way, since a credential file contributes a path and an
instance name and nothing else. Never put `OSW_PASSWORD` inline in a committed
`.mcp.json`.

```json
{
"mcpServers": {
"osw": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "osw[mcp]", "osw-mcp"],
"env": {
"OSW_CRED_FILEPATH": "/abs/path/to/accounts.pwd.yaml",
"OSW_DOMAIN": "wiki-dev.open-semantic-lab.org"
}
}
}
}
```

At startup the server checks that the credential file has an entry matching
`OSW_DOMAIN` and, if not, names the iris the file does contain (never their
secrets), so a typo surfaces immediately rather than on the first tool call.

Registering the same entry from a shell is easiest with `add-json`, which takes
it verbatim. Note that a Windows path needs forward slashes or doubled
backslashes to be valid JSON:

```bash
claude mcp add-json osw '{"type":"stdio","command":"uvx","args":["--from","osw[mcp]","osw-mcp"],"env":{"OSW_CRED_FILEPATH":"/abs/path/to/accounts.pwd.yaml","OSW_DOMAIN":"wiki-dev.open-semantic-lab.org"}}'
```

### More than one instance

Register one server per instance, each pinned to a single `OSW_DOMAIN`. The two
entries below show both styles side by side: `osw-dev` puts everything in a
`.env` file, `osw-prod` names the credential file and the domain directly. One
credential file can serve any number of servers, since it is keyed by iri.

```json
{
"mcpServers": {
"osw-dev": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "osw[mcp]", "osw-mcp"],
"env": { "OSW_ENV_FILE": "/abs/path/to/dev.env" }
},
"osw-prod": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "osw[mcp]", "osw-mcp"],
"env": {
"OSW_CRED_FILEPATH": "/abs/path/to/accounts.pwd.yaml",
"OSW_DOMAIN": "wiki.open-semantic-lab.org",
"OSW_READ_ONLY": "true"
}
}
}
}
```

`dev.env` has to pin the instance itself, since the server will not infer one:

```dotenv
OSW_DOMAIN=wiki-dev.open-semantic-lab.org
OSW_CRED_FILEPATH=/abs/path/to/accounts.pwd.yaml
```

The instance is then part of the tool name at every call site
(`mcp__osw-prod__get_entity`), so the destination is visible in the permission
prompt, read-only is settable per instance, and permissions can differ per
instance:

```json
{
"permissions": {
"allow": ["mcp__osw-dev"],
"ask": ["mcp__osw-prod"]
}
}
```

### Notes and caveats

- `status` reports the active instance and connection state, never the password.
- **Safe deletes:** the server records every entity it creates or modifies in a
local provenance ledger. It deletes those without extra prompting, but refuses
to delete anything it did not create unless the caller passes
`confirm_external_delete=true`.

## Configuration

Both adapters share the settings below.

### Where settings come from

Settings are read from the process environment. A `.env` file is one optional
way to fill it, and a real environment variable always wins over the same name
in a file.

- `OSW_ENV_FILE` set: exactly that file is loaded, and nothing is searched for.
- Unset, **CLI**: searches upward from the working directory, so a `.env` in a
project root applies to every `osw` command run anywhere inside it.
- Unset, **MCP server**: searches nowhere. Its working directory is picked by
the MCP client, so an implicit search would tie the credentials it loads to
how the client happened to be launched.

Both print the sources they resolved to stderr before connecting:

```text
[osw] env file : /home/me/project/.env (found from the working directory upward)
[osw] credential file: /abs/path/to/accounts.pwd.yaml
```

### Credentials

Keep credentials in a gitignored file. They are read once per process, into that
process only, and never written back to disk.

```dotenv
OSW_DOMAIN=wiki-dev.open-semantic-lab.org
OSW_USERNAME=your-user
OSW_PASSWORD=your-password
# optional
OSW_SPARQL_ENDPOINT=https://.../sparql
OSW_READ_ONLY=false # true hides all mutating tools
```

Alternatively, authenticate from an osw credential file, so the password is not
duplicated into a second plaintext file:

```dotenv
OSW_DOMAIN=wiki-dev.open-semantic-lab.org
OSW_CRED_FILEPATH=/abs/path/to/accounts.pwd.yaml
```

The file is the YAML format osw's `CredentialManager` already reads, keyed by
iri, so deployments that configure it need no extra setup:

```yaml
wiki-dev.open-semantic-lab.org:
username: your-user
password: your-password
```

A credential file may hold several iris. The CLI selects one automatically if it
is the only one, and otherwise wants `osw --instance <iri>`. The MCP server
never selects one, see
[One server per instance](#mcp-server).

### Variable reference

The canonical variable names are `OSW_*`. Older `OSW_MCP_*` and `OSL_*` names
stay accepted so existing deployments keep working, and the first name that is
set wins:

| Canonical | Also accepted | Meaning |
| --- | --- | --- |
| `OSW_DOMAIN` | `OSL_DOMAIN` | Instance to connect to |
| `OSW_USERNAME` | `OSL_USERNAME` | Login user |
| `OSW_PASSWORD` | `OSL_PASSWORD` | Login password |
| `OSW_CRED_FILEPATH` | `OSW_MCP_CRED_FILEPATH`, `OSL_CRED_FILEPATH` | YAML credential file, keyed by iri |
| `OSW_ENV_FILE` | `OSW_MCP_ENV_FILE` | `.env` file to load |
| `OSW_READ_ONLY` | `OSW_MCP_READ_ONLY` | `true` refuses every write |
| `OSW_SPARQL_ENDPOINT` | | Endpoint for `sparql` queries |
| `OSW_STATE_DIR` | `OSW_MCP_STATE_DIR` | Where the provenance ledger is kept |
| `OSW_MAX_RESULTS` | `OSW_MCP_MAX_RESULTS` | Default result cap (100) |
| `OSW_MAX_CHARS` | `OSW_MCP_MAX_CHARS` | Result size cap in characters (100000) |

### Windows paths in a `.env` file

Quote them with single quotes, or leave them unquoted. A double-quoted value is
escape-decoded, so `\a` in a path silently becomes a BEL byte that renders as
nothing:

```dotenv
OSW_CRED_FILEPATH='C:\Users\me\accounts.pwd.yaml' # ok
OSW_CRED_FILEPATH=C:\Users\me\accounts.pwd.yaml # ok
OSW_CRED_FILEPATH="C:\Users\me\accounts.pwd.yaml" # broken: \a is eaten
```

## Design notes

Why the two adapters are shaped the way they are:

- **No filesystem access on the MCP surface.** MCP does not imply a shared host:
a server can be containerised or remote, so a path argument is either
meaningless or a way to reach a filesystem nobody granted access to. A CLI
runs where the command was typed, under that user's own permissions, and an
agent calling it goes through whatever command permissions already apply.
- **One instance per server process.** Which instance a tool call reaches has to
be readable from the configuration rather than inferred, so the server never
picks one for you, not even when the credential file holds exactly one iri.
- **stdio only.** SSE is deprecated upstream, and HTTP would need a
per-connection auth model this server does not have: it holds one set of wiki
credentials, which every client would share.
- **`mcp` is an extra, not a base dependency.** The SDK pulls in a server stack
(starlette, uvicorn, sse-starlette) that nothing in the Python API or the CLI
needs, so only users who actually run the server pay for it.

## Notes for developers

To try an unreleased branch against a real client, point `uvx` at the checkout
instead of at PyPI. Everything else about the registration stays the same:

```bash
uvx --reinstall --from "/abs/path/to/osw-python[mcp]" osw-mcp
```

`--reinstall` is what picks up your latest edits, since `uvx` caches the wheel
it builds. In a JSON `args` array, a Windows path needs forward slashes or
doubled backslashes.

Prefer that over an editable install for the server. `create_or_update_entity`
and `export_entity_jsonld` call `fetch_schema`, which regenerates
`src/osw/model/entity.py` inside the installed package: `uvx` builds a
non-editable wheel, so the write lands in the uv cache, while under
`pip install -e` or `uv sync` it lands in your working tree. The read tools
(`get_entity`, `get_slot`, `get_category_schema`, ...) read raw page slots and
never trigger it.
1 change: 1 addition & 0 deletions docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
| `osw[S3]` | Interact with S3 stores per S3FileController |
| `osw[dataimport]` | Additional tools to import data |
| `osw[UI]` | To use a helper UI to work with entity slots |
| `osw[mcp]` | [MCP server](cli-and-mcp.md#mcp-server) for agent clients |
| `osw[all]` | All of the above |

Install multiple extras with `pip install osw[opt1,opt2]`.
Expand Down
Loading
Loading