Skip to content

Commit 0455673

Browse files
authored
feat: an osw CLI and an MCP server for live OSL instances (#133)
* feat(mcp): add osw-mcp server exposing a live OSL instance Add an in-repo `osw[mcp]` extra and an `osw-mcp` stdio console script that wraps OswExpress and serves it over the Model Context Protocol for clients such as Claude Code. Tools: semantic/SPARQL/full-text search, category schema introspection, entity read + JSON-LD export, create/update/delete, full page-slot access, and file up/download. - Delete is provenance-guarded: a local JSON ledger records pages the server created/modified; deleting anything untracked requires confirm_external_delete=true. - Credentials resolve from env/.env and are validated up front (fail fast, never prompts, so stdio is never corrupted by an input() call). - osw stdout is redirected to stderr so it never leaks onto the JSON-RPC channel. - OSW_MCP_READ_ONLY hides all mutating tools. * feat(mcp): port server to mcp 2.x and isolate the extra - FastMCP replaced by MCPServer, extra now requires mcp>=2 - mcp dropped from the all extra and the dev group: it needs anyio>=4.9, workflow pins anyio<4.7 (#139) - uv conflicts declare mcp exclusive with workflow and with dev - pytest stack moved to its own test group, so an environment with both pytest and mcp exists - src/osw/mcp excluded from ty, mcp tests guarded by importorskip * feat(mcp): authenticate from an osw credential file - OSW_MCP_CRED_FILEPATH configures it, OSL_CRED_FILEPATH is a fallback - an alternative to OSW_USERNAME/OSW_PASSWORD, so the password is not duplicated into a second plaintext file - existence and a matching domain entry are validated at startup - lookups use CredentialFallback.none, so osw never prompts and never blocks the stdio transport * feat(mcp): select between multiple OSL instances at runtime - list_instances and select_instance tools, returning iris only and never any credential value - OSW_DOMAIN becomes optional when a credential file supplies the iris - auto-selects when OSW_DOMAIN is set or the file holds exactly one iri - switching rebuilds the connection and the per-domain provenance ledger - tools resolve the active domain and credentials at call time * refactor: extract SDK-free osw.service core from osw.mcp - move config/ledger/serialization from osw.mcp to osw.service - add errors.py (OpError), context.py (Context+Policy), registry.py - Operation validator rejects path-like params on the mcp surface - canonical OSW_* env names, OSW_MCP_*/OSL_* kept as aliases - config/ledger/serialization tests now run without the mcp extra * refactor(service): lift search tools into osw.service.ops - add osw/service/ops/ with the search group as @operation functions - mcp/tools/search.py becomes a registry loop over bind() - add transitional legacy_context() so existing tests keep passing - bind() resolves annotations against the op module, not registry.py * refactor(service): lift remaining tool bodies into osw.service.ops - schema, status, entities and slots lifted as @operation functions - mcp/tools/*.py collapse to registry loops over bind() - error dicts become raised errors.*; ledger calls become records= hooks - normalize cli_name so every group reads as `osw <group> <verb>` - 172 passed in the dev env, 29 in the mcp extra env * feat(cli): add typer CLI assembled from the operation registry - typer as a base dependency; osw = "osw.cli.main:app" console script - commands built from iter_operations(surface="cli"); Context built lazily - OpError.exit_code becomes the process exit status, no traceback - json_value parser lives in osw.service.params, so core never imports cli - set_slot coerces content per the sibling slot's content model * feat: remove filesystem paths from the MCP surface - add path-free get_file_info/read_file_text/write_file_text built on WikiFileController.get()/.put(), never touching the local filesystem - move file download/upload and ledger path to osw.cli.ops, the only module allowed to name a path (surfaces={"cli"}); drop status's ledger_path - guard tests: no MCP-surfaced op names a path, and osw.mcp.server never imports osw.cli - read_file_text decodes incrementally so a byte cap splitting a multi-byte character is not misreported as binary content * feat(mcp): pin one instance per server, wire annotations and meta - delete list_instances/select_instance; each server process is pinned to one OSL instance and refuses to start if none resolves - map Operation's hints onto ToolAnnotations and _meta by explicit keyword, since the SDK silently absorbs a misspelled field name - set server instructions and version; make mcp.run(transport="stdio") explicit - add CLI --instance and osw instance list, returning iris only - retarget the "no instance selected" message at OSW_DOMAIN and --instance instead of the removed tool * refactor(mcp): drop tool closures, fold registration into server - delete osw/mcp/tools/ and connection.py; bodies live in osw.service.ops - fold registration.py into server.py, its sole consumer - replace test_mcp_tools.py with test_service_ops.py + test_mcp_server.py - rebuild the integration fixture on bind()/iter_operations - test_mcp_instances.py -> test_service_instances.py, no longer SDK-gated * build: type-check src/osw/mcp instead of excluding it - drop src/osw/mcp from [tool.ty.src] exclude - silence only the two unresolvable SDK imports inline (issue #139) * docs: document the osw CLI and unify the config reference - add a Command line section: command tree, global options, exit behaviour - move credentials into a shared Configuration section with an alias table - state that no MCP tool takes a path, and where the path-based commands live - add stdio type, multi-instance and per-instance permission examples - give each CLI command group a one-line help string * fix(config): find .env from the CWD, report config sources - CLI now searches upward from the working directory, not from the installed package's directory (dotenv's default walks the call stack) - MCP server searches nowhere: its CWD is chosen by the client - both print the resolved .env and credential file to stderr at startup - a missing credential file whose path holds a control character now explains .env double-quote escape decoding * docs: move CLI and MCP sections out of the README - new docs page "CLI and MCP tools", added to the zensical nav - README keeps a short pointer section, otherwise back to its old shape - get-started extras table gains the osw[mcp] row * docs: default MCP examples to a credential file in env - server entries now set OSW_CRED_FILEPATH plus OSW_DOMAIN, no .env needed - multi-instance example shares one credential file, one domain per server - register via claude mcp add-json, which takes the entry verbatim * feat(mcp): require an explicitly configured OSW_DOMAIN - server no longer auto-selects a single-iri credential file; the CLI still does - docs present the env block and the .env file as two supported styles - multi-instance example shows one server of each style - drop the "OSW_DOMAIN may be omitted" note * docs: restructure the CLI and MCP guide - add a Setup section with the uv/pip installs up front - move Configuration below the MCP section, both adapters share it - state where the .env is looked for first, drop the always-loaded claim - note that --instance is optional and when it is required - collect the rationale in a Design notes section at the end * docs: simplify the CLI and MCP setup section - lead with uv tool install, the mcp extra includes the base package - fold the pip, uv add and uvx variants into a details element - move the editable-install caveat to a Notes for developers section - document running the server from a local checkout via uvx --from * added local folders to .gitignore * docs: correct why the CLI may infer an instance - --instance is optional, so it is not what makes inference acceptable - the CLI resolves per invocation and reports the instance it resolved - state the --instance condition as OSW_DOMAIN unset, not .env-specific * refactor: de-isolate the mcp extra from the dev environment - drop the ty: ignore on the mcp SDK imports in server.py - run the MCP tests unconditionally instead of importorskip-ing them - update the config.py env-file hint (no more `test` group) - docs: mcp is part of osw[all]; replace the anyio design note * docs: drop remaining references to the separate MCP environment - test docstrings no longer contrast against a plain dev env - deptry comment no longer claims extras are absent from dev - README lists osw[mcp] among the extras * refactor(service): validate Settings with pydantic - convert Settings from a frozen dataclass to a frozen pydantic model - add validators for domain, sparql_endpoint, state_dir, cred_filepath - constrain max_results/max_chars to positive integers via Field(gt=0) - drop _int_env in favour of one ValidationError -> RuntimeError site that still names the exact alias that was set Closes #143 * test: stop test_init_from_env_vars leaking OSW_CRED_FILEPATH - use monkeypatch.setenv so OSW_CRED_FILEPATH and OSW_DOMAIN are restored - the test unlinks its credential file, so the leaked path pointed every later test at a missing file - surfaced by the mcp de-isolation: tests/integration/test_mcp_server.py no longer skips for a missing SDK, so it hit the polluted environment * test: do not assume the first ask-query hit carries jsondata - SMW ask results have no defined order and Category:Item can hold pages without a jsondata slot - scan all returned titles, require at least one with the slot * fix(service): validate read_only via pydantic instead of truthy set - route OSW_READ_ONLY through Settings so an unparseable value raises - a typo like "ture" previously yielded False, silently enabling writes - "y"/"t" now parse as true; all documented spellings keep working - blank/whitespace-only still falls back to the default, as for the ints - drop the now-unused _TRUTHY set Follow-up to #143. * fix(deps): make python-dotenv a base dependency - the osw CLI is a base console script, but .env loading required osw[mcp] - without dotenv the implicit .env search returned silently, so the CLI reported missing credentials instead of the missing package - drop the now redundant entry from the mcp extra - update the docstrings and error message that named osw[mcp] * feat(cli): find accounts.pwd.yaml in the working directory - look for accounts.pwd.yaml in the CWD when no credential file is set - skip the lookup when OSW_USERNAME or OSW_PASSWORD is configured - ignore a discovered file with no entry for the configured domain - report the credential file and where its path came from - no parent-directory walk, and the MCP server never searches * feat(cli): print one configuration line by default - reduce the CLI banner to the credential source unless --verbose - print the env-file line too when a command fails - omit the MCP stdio hint from CLI error messages - flush the banner so it survives a buffered stderr * docs(mcp): add a quick install section for claude mcp add - one claude mcp add command with the env block inline - note on quoting osw[mcp] and on Windows paths - scope flags, and how to list, inspect and remove the entry * docs(cli): make the search help explain OSW-ID titles - rename full_text_search to search_titles: it searches titles only - drop "full-text" from the group help, module docstring and docs - state that OSW pages are titled by OSW-ID, so names are not in titles - add a wildcard label example to `search ask`, with the caveat that the property name depends on the instance schema (not run against a wiki) - document the --limit default and the {titles, count, truncated} shape * feat(search): add a page-content search - wiki_tools.content_search wraps the MediaWiki list=search API - WtSite.content_search delegates to it, like prefix_search - new operation search_content, CLI `osw search content`, MCP tool - returns page titles in the same shape as the sibling searches * docs(cli): rename `search text` to `search titles` - `text` next to the new `content` implied it searched page text - ground the name-property advice in Entity.json's @context mapping - name the jsondata slot in the content-search caveat - list `titles` and `content` in the command table * docs(mcp): describe the credential lookup without the rationale - replace the env-file prose with a CLI/server table - state the credential-file order as steps, drop the reasons - merge the duplicated credential setup into one section - fix quick install: osw-mcp is the console script, not the server name - correct the default `claude mcp add` scope to local * feat(mcp): report config sources only when OSW_VERBOSE is set - add Settings.verbose, read from OSW_VERBOSE or OSW_MCP_VERBOSE - MCP server buffers the credential and env-file lines instead of writing them to stderr at every start - a failed start still prints both lines, verbose or not - CLI output is unchanged * test(mcp): cover main() on a successful start, quiet and verbose * fix(service): flag truncation at the limit that reached the wiki - cap_list only reports a cut it made itself, but all four search operations pass the same limit to the wiki, which never returns more, so `truncated` was always False - flag when the result set is as large as the effective limit - search_entities: a `limit=` inside the ask query wins over the limit argument, so compare against that one - sparql_query unchanged; its limit applies to the response, not the query * docs(service): say what search_entities actually counts * docs: reorganize CLI and MCP pages into a Tools section - replace the "CLI and MCP tools" tab with a "Tools" tab - split the single page into Overview, CLI, MCP and Configuration - leave room for a User Sync page from PR #174 - repoint the links in README.md and get-started.md * feat(cli): add instances status, fix the username status reports - status reports the username from a credential file, not only from OSW_USERNAME, and uses the precedence the login path uses - rename the "instance" group to "instances", unconfusable with --instance - add "osw instances status" to check every configured instance at once - name the correct form when a root option is typed after the command - extract config.get_credentials_for and Context.osw_for * fix(service): print the running adapter's name in shared messages - shared code hardcoded [osw] or [osw-mcp], so each adapter printed the other one's name about half the time - config.set_log_prefix/log_prefix; CLI sets osw, MCP server sets osw-mcp - covers the default delete and set_slot wiki edit comments, which are written permanently into page histories - add flush=True to the status connection-failure print, which CliRunner otherwise never captures because it flushes only stdout * refactor(cli): rename osw search instances to osw search entities - "instances" read as OSL instances, which is what osw instances lists - MCP tool name is the function name, so list_instances_of_category becomes search_entities and the ask query becomes search_ask - fix the help text, which documented [[HasType::Category:<category>]] while the real query for Category:Item is [[HasType::Category:Item]] - set the MCP prefix in the integration fixture that stands in for the server, so a future write test cannot tag a live wiki edit as [osw] * test(logging): reset the osw logger in its fixture The tests assumed osw was imported after something had configured logging, which decides whether osw attaches its own handler on import. tests/conftest.py now imports osw, so that assumption no longer held and the hand-over test read the leftover import-time handler. * refactor(service): log shared diagnostics instead of printing them - six stderr prints in the shared code move to the osw logger - removes messages lost to an unflushed stderr write - the configuration source report stays a print, gated by --verbose - the MCP startup failure stays a print, so OSW_LOG_LEVEL=OFF cannot hide it * build: declare click as a direct dependency osw.cli.main subclasses click's Command and Group to report a misplaced root option, so click is imported directly. deptry flagged it as DEP003 and failed the quality job. * docs(service): correct the reason the config report flushes sys.stderr is line-buffered on every supported Python (>=3.10). The real reason is the caller-chosen stream: the MCP server passes an io.StringIO and click's CliRunner never flushes its stderr replacement. * test(cli): normalize typer's rich output before asserting - typer forces colour on when GITHUB_ACTIONS is set - rich styles an option's leading dash separately, so "--jsondata" is no longer a plain substring - rich wraps at the real terminal width, 80 in CI, 79 on Windows - strip the escape sequences and panel borders, then join the lines * fix(cli): write stdout and stderr as UTF-8, not the locale encoding - a redirected stream used cp1252 on Windows, so a German label reached the consumer as bytes no JSON parser could read, with exit code 0 - a character cp1252 cannot represent raised UnicodeEncodeError instead - the app callback reconfigures both streams before any output - errors= is passed too, or reconfigure would reset stderr to strict - --help and an unknown command name still bypass it; neither carries wiki content Closes #187 * feat(cli): suppress osw's import notice on the console scripts - osw and osw-mcp now start through the new src/osw_entry.py shim - the shim sets OSW_LOG_LEVEL before osw is imported, which is the only place that can, since importing any submodule imports the package first - setdefault leaves a value the caller already chose alone - a plain "import osw" still writes the notice, library behaviour is deliberately unchanged - the wheel ships osw_entry.py via only-include plus sources * test(cli): cover the osw-mcp script and the shim's level choice - the osw-mcp console script had no test of its own, only osw did - it now runs as a subprocess and must leave stdout empty, which is the assertion that matters for a JSON-RPC transport - the level the shim sets moved into _DEFAULT_LEVEL, and a test ties it to osw.DEFAULT_LOG_LEVEL so the two cannot drift apart * fix(config): validate domain and state_dir (#195) * fix(config): reject a hostless domain and a relative state_dir - domain accepts a bare host or a full URL, so the check is on what _derive_domain yields rather than on the form of the value - a hostless domain used to fail later in OswExpress.validate_domain, quoting a regex instead of naming OSW_DOMAIN - state_dir "~/osw" created a directory literally named "~", because Ledger builds its path with Path() and never expands it - a relative state_dir resolves against a working directory the MCP client chooses, so the ledger landed in an unpredictable place - last open item from #143 * test(config): cover drive-relative state_dir and an unknown home - reject "\osw-state" and "C:osw-state", non-absolute on both platforms - assert the expanduser RuntimeError becomes a named config error - state in the docs that a domain without a host is rejected * test: decode subprocess output as UTF-8, not the locale codepage - text=True alone decodes with locale.getencoding(), which ignores Python's UTF-8 mode, so a child writing UTF-8 broke the reader thread with UnicodeDecodeError and returned the stream as None - reproduced on Windows: byte 0x90 at position 1028 of osw --help - the osw_entry tests now share a _DECODE mapping - test_no_paths_on_mcp_surface gets the same kwargs as protection
1 parent e927526 commit 0455673

59 files changed

Lines changed: 9064 additions & 19 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,8 @@ playground
6969
/osw_files/
7070
*/accounts.pwd.yaml
7171
/accounts.pwd.yaml
72-
.ign
73-
.claude
72+
73+
# Local folders
74+
.ign/
75+
.claude/
76+
graphify-out/

‎README.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pip install osw
2222
```
2323

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

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

42+
## Tools
43+
44+
Installing `osw` also installs an `osw` command line client, and the
45+
`osw[mcp]` extra adds an MCP server that exposes a live instance to agent
46+
clients such as Claude Code:
47+
48+
```bash
49+
osw search ask '[[Category:Item]]' --limit 5
50+
```
51+
52+
Commands, tools and their configuration are described in the
53+
[Tools guide](https://opensemanticlab.github.io/osw-python/tools/).
54+
4255
## Logging
4356

4457
osw reports what it is doing on the `osw` logger at INFO by default. Levels,

‎docs/get-started.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
| `osw[S3]` | Interact with S3 stores per S3FileController |
3131
| `osw[dataimport]` | Additional tools to import data |
3232
| `osw[UI]` | To use a helper UI to work with entity slots |
33+
| `osw[mcp]` | [MCP server](tools/mcp.md) for agent clients |
3334
| `osw[all]` | All of the above |
3435

3536
Install multiple extras with `pip install osw[opt1,opt2]`.

‎docs/tools/cli.md‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# CLI
2+
3+
## Quick start
4+
5+
Both adapters need an instance and credentials. The quickest start is a
6+
gitignored `.env` file in your project root:
7+
8+
```dotenv
9+
OSW_DOMAIN=wiki-dev.open-semantic-lab.org
10+
OSW_USERNAME=your-user
11+
OSW_PASSWORD=your-password
12+
```
13+
14+
The CLI searches upward from the working directory for it, so `osw status`
15+
now reports the instance, the username (whether it comes from `OSW_USERNAME`
16+
or a credential file), and connection state. The MCP server takes its
17+
settings from the `env` block of its registration instead, see
18+
[Registering a server](mcp.md#registering-a-server). Every variable is listed
19+
under [Configuration](configuration.md).
20+
21+
## Command line
22+
23+
```bash
24+
osw status
25+
osw search ask '[[Category:Item]]' --limit 5
26+
osw entity get 'Item:OSW1234...' --json | jq .
27+
osw file cat 'File:Example.csv' # inline text
28+
osw file download 'File:Example.csv' --target-dir ./tmp # to disk
29+
```
30+
31+
Commands are grouped by subject:
32+
33+
| Group | Commands |
34+
| --- | --- |
35+
| `entity` | `get`, `put`, `export`, `delete` |
36+
| `file` | `info`, `cat`, `write`, `download`, `upload` |
37+
| `search` | `ask`, `titles`, `content`, `entities`, `sparql` |
38+
| `slot` | `list`, `get`, `set` |
39+
| `schema` | `get` |
40+
| `instances` | `list`, `status` |
41+
| `ledger` | `path` |
42+
| top level | `status` |
43+
44+
`osw search entities` finds pages in the wiki that are instances of a
45+
category, while `osw instances` is about the OSL servers this process can
46+
connect to.
47+
48+
`osw instances list` lists the iris the process can connect to: the
49+
env-configured domain plus every entry of a configured credential file.
50+
51+
`osw instances status` reports the same instances in more detail. For each
52+
one it prints the iri, whether it is the active one, the username that would
53+
be used, and whether a connection succeeded. It never prints passwords. The
54+
instances are contacted one after another and a single attempt has no
55+
timeout, so an unreachable instance delays the command until its connection
56+
attempt gives up.
57+
58+
Global options apply to every command. They belong to the `osw` command
59+
itself, so they come before the subcommand, the same way `git` and `docker`
60+
options do: `osw --instance <iri> status`, not `osw status --instance <iri>`.
61+
Typing them after the subcommand now produces an error that names the correct
62+
form.
63+
64+
- `--instance IRI` picks the instance. Optional: it is only required when
65+
`OSW_DOMAIN` is not set and the configured credential file holds more than
66+
one iri.
67+
- `--json` / `-j` writes machine-readable JSON to stdout and keeps osw's own
68+
progress output on stderr, so it pipes cleanly into `jq`.
69+
- `--read-only` refuses write operations.
70+
- `--verbose` / `-v` shows full tracebacks instead of a one-line message, and
71+
adds the env-file line to the source report described under
72+
[Where settings come from](configuration.md#where-settings-come-from).
73+
74+
Failures exit non-zero with a short message on stderr and no traceback.

‎docs/tools/configuration.md‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Configuration
2+
3+
Both adapters share the settings below.
4+
5+
## Where settings come from
6+
7+
Settings are read from the process environment. A `.env` file fills that
8+
environment; a real environment variable wins over the same name in a file.
9+
10+
**Env file**
11+
12+
| `OSW_ENV_FILE` | CLI | MCP server |
13+
| --- | --- | --- |
14+
| set | loads that file, searches nowhere | loads that file, searches nowhere |
15+
| unset | searches upward from the working directory | searches nowhere |
16+
17+
**Credential file.** The first step that produces a file wins:
18+
19+
1. `OSW_CRED_FILEPATH` or an alias, set in the environment or the env file.
20+
The run fails if this file has no entry for `OSW_DOMAIN`. That check is
21+
skipped when `OSW_USERNAME` and `OSW_PASSWORD` are both set.
22+
2. CLI only: `accounts.pwd.yaml` in the working directory. Parent directories
23+
are not searched. This step is skipped when `OSW_USERNAME` or
24+
`OSW_PASSWORD` is set. If the file has no entry for `OSW_DOMAIN` it is
25+
ignored and the run continues.
26+
3. No credential file.
27+
28+
**Source report.** Both adapters write to stderr before connecting. The first
29+
line is labelled `credential file` when a file was found:
30+
31+
- `<path> (from the OSW_CRED_FILEPATH environment variable)`
32+
- `<path> (from OSW_CRED_FILEPATH in the env file)`
33+
- `<path> (accounts.pwd.yaml found in the working directory)`
34+
- `<path> (accounts.pwd.yaml found in the working directory, ignored: no entry for domain '<domain>')`
35+
36+
and `credentials` when none was:
37+
38+
- `OSW_USERNAME/OSW_PASSWORD (from the environment)`
39+
- `OSW_USERNAME/OSW_PASSWORD (from the env file)`
40+
- `not configured (set OSW_CRED_FILEPATH, or OSW_USERNAME/OSW_PASSWORD)`
41+
42+
The second line is labelled `env file`. Which lines appear depends on the
43+
adapter:
44+
45+
- **CLI**: the first line only. `--verbose`, or a command that fails, adds
46+
the second.
47+
- **MCP server**: neither, since its sources are fixed in the server entry.
48+
`OSW_VERBOSE=true` prints both, and a failed start prints both regardless.
49+
50+
A verbose run of the CLI prints:
51+
52+
```text
53+
[osw] credential file: /home/me/project/accounts.pwd.yaml (accounts.pwd.yaml found in the working directory)
54+
[osw] env file : /home/me/project/.env (found from the working directory upward)
55+
```
56+
57+
The prefix names the adapter that printed the line: `[osw]` for the CLI,
58+
`[osw-mcp]` for the MCP server. This holds for every message the two share,
59+
not only these two lines.
60+
61+
## Where messages go
62+
63+
The source report above is printed directly, because the adapter's own verbose
64+
flag decides whether it appears, not the log level.
65+
66+
Every other message the adapters produce goes to the `osw` logger, together
67+
with the records of the library itself. A failed connection check and an
68+
unreadable provenance ledger are reported that way. `OSW_LOG_LEVEL` sets how
69+
much of it appears, and an application that configures logging itself takes the
70+
records over. See [Logging](../get-started.md#logging).
71+
72+
Both kinds of message are written to stderr, never to stdout. The MCP server
73+
speaks JSON-RPC over stdout, and the CLI writes its `--json` output there, so
74+
stdout has to stay free.
75+
76+
## Credentials
77+
78+
Keep credentials in a gitignored file. They are read once per process, into that
79+
process only, and never written back to disk. Set either `OSW_USERNAME` and
80+
`OSW_PASSWORD`, or `OSW_CRED_FILEPATH`.
81+
82+
A credential file uses the YAML format osw's `CredentialManager` reads, keyed
83+
by iri (default file name: `accounts.pwd.yaml`):
84+
85+
```yaml
86+
wiki-dev.open-semantic-lab.org:
87+
username: your-user
88+
password: your-password
89+
```
90+
91+
A credential file may hold several iris. The CLI selects one automatically if it
92+
is the only one, and otherwise requires `osw --instance <iri>`. The MCP server
93+
never selects one, see [One server per instance](mcp.md).
94+
95+
## Variable reference
96+
97+
The canonical variable names are `OSW_*`. Older `OSW_MCP_*` and `OSL_*` names
98+
stay accepted so existing deployments keep working, and the first name that is
99+
set wins:
100+
101+
| Canonical | Also accepted | Meaning |
102+
| --- | --- | --- |
103+
| `OSW_DOMAIN` | `OSL_DOMAIN` | Instance to connect to. A bare host (`wiki.example.org`) or a full URL (`https://wiki.example.org/w/`); the host is taken from either, and a value no host can be read from (`https://`, `/w/index.php`) is rejected at startup |
104+
| `OSW_USERNAME` | `OSL_USERNAME` | Login user |
105+
| `OSW_PASSWORD` | `OSL_PASSWORD` | Login password |
106+
| `OSW_CRED_FILEPATH` | `OSW_MCP_CRED_FILEPATH`, `OSL_CRED_FILEPATH` | YAML credential file, keyed by iri (falls back to `accounts.pwd.yaml` in the working directory, CLI only) |
107+
| `OSW_ENV_FILE` | `OSW_MCP_ENV_FILE` | `.env` file to load |
108+
| `OSW_READ_ONLY` | `OSW_MCP_READ_ONLY` | `true` refuses every write |
109+
| `OSW_SPARQL_ENDPOINT` | | Endpoint for `sparql` queries |
110+
| `OSW_STATE_DIR` | `OSW_MCP_STATE_DIR` | Where the provenance ledger is kept. Must be an absolute path; a leading `~` is expanded |
111+
| `OSW_MAX_RESULTS` | `OSW_MCP_MAX_RESULTS` | Default result cap (100) |
112+
| `OSW_MAX_CHARS` | `OSW_MCP_MAX_CHARS` | Result size cap in characters (100000) |
113+
| `OSW_VERBOSE` | `OSW_MCP_VERBOSE` | `true` prints the configuration source report |
114+
115+
## Windows paths in a `.env` file
116+
117+
Quote them with single quotes, or leave them unquoted. A double-quoted value is
118+
escape-decoded, so `\a` in a path silently becomes a BEL byte that renders as
119+
nothing:
120+
121+
```dotenv
122+
OSW_CRED_FILEPATH='C:\Users\me\accounts.pwd.yaml' # ok
123+
OSW_CRED_FILEPATH=C:\Users\me\accounts.pwd.yaml # ok
124+
OSW_CRED_FILEPATH="C:\Users\me\accounts.pwd.yaml" # broken: \a is eaten
125+
```

‎docs/tools/index.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Tools
2+
3+
Besides the Python API, osw ships two adapters that talk to a live instance:
4+
the `osw` command line client, and an MCP server for agent clients such as
5+
Claude Code. Both run the same operations from one shared, SDK-free core
6+
(`osw.service`), so a command and its matching tool behave identically. They
7+
differ in exactly one way: only the CLI accepts filesystem paths.
8+
9+
## Setup
10+
11+
Install one of the two; the second includes the first:
12+
13+
```bash
14+
uv tool install osw # the `osw` command
15+
uv tool install "osw[mcp]" # the same, plus the `osw-mcp` server
16+
```
17+
18+
<details markdown="1">
19+
<summary>Other ways to install</summary>
20+
21+
```bash
22+
pip install "osw[mcp]" # into the active environment
23+
uv add "osw[mcp]" # as a dependency of the current uv project
24+
uvx --from "osw[mcp]" osw-mcp # run the server without installing it
25+
```
26+
27+
`uvx` is what the registration examples further down use, so the server needs
28+
no install of its own.
29+
30+
</details>
31+
32+
`osw[mcp]` is also part of `osw[all]`. The other extras are listed in the
33+
[Get Started guide](../get-started.md#optional-extras).
34+
35+
## In this section
36+
37+
| Page | Contents |
38+
| ---- | -------- |
39+
| [CLI](cli.md) | The `.env` quick start, the full command reference, and the global flags |
40+
| [MCP server](mcp.md) | The tool surface, the no-filesystem-access and one-server-per-instance rules, and how to register the server with a client such as Claude Code |
41+
| [Configuration](configuration.md) | What both adapters share: where credentials and settings come from, and the full environment-variable reference |

0 commit comments

Comments
 (0)