Skip to content

feat: an osw CLI and an MCP server for live OSL instances - #133

Open
LukasGold wants to merge 30 commits into
mainfrom
feat/mcp-server
Open

feat: an osw CLI and an MCP server for live OSL instances#133
LukasGold wants to merge 30 commits into
mainfrom
feat/mcp-server

Conversation

@LukasGold

@LukasGold LukasGold commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What this adds

Two ways to work against a live OpenSemanticLab instance without writing Python:

  • osw, a command line client, shipped with the base package
  • osw-mcp, a stdio MCP server (osw[mcp] extra) that exposes one instance
    to agent clients such as Claude Code

Both are generated from a single declaration per operation, so a command and its
matching tool cannot drift apart in behaviour, argument names or help text.

Capabilities

Subject CLI MCP tools
Search search ask, search text, search instances, search sparql search_entities, full_text_search, list_instances_of_category, sparql_query
Schemas schema get get_category_schema
Entities entity get, put, export, delete get_entity, create_or_update_entity, export_entity_jsonld, delete_entity
Page slots slot list, get, set list_page_slots, get_slot, set_slot
File pages as text file info, cat, write get_file_info, read_file_text, write_file_text
Files on disk file download, file upload none, by design
Session status, instance list, ledger path status

Behaviour common to both:

  • Read-only mode (--read-only, OSW_READ_ONLY) hides mutating tools rather
    than failing them when called, so an agent never sees a tool it cannot use.
  • Provenance-guarded deletes. Pages the server created or modified delete
    normally; anything else requires confirm_external_delete=true.
  • Result caps (OSW_MAX_RESULTS, OSW_MAX_CHARS) keep a broad query from
    flooding an agent's context.
  • Credentials come from the environment, a .env file, or an iri-keyed osw
    credential file. No tool ever returns one.
  • Machine-readable output. osw --json puts JSON on stdout and osw's own
    progress output on stderr. Failures exit non-zero with one line, no traceback.

Constraints that shaped it

Constraint Consequence
An MCP server can be remote or containerised no filesystem path on the MCP surface
stdio carries the JSON-RPC stream no prompt, no stdout write; osw's own output is redirected to stderr
Wiki content an agent reads is untrusted input no runtime instance switching; deletes are provenance-guarded
Whatever a tool returns enters the agent's context no credential value crosses MCP, ever

Decisions that need agreement

1. No filesystem path reaches the MCP surface.
Rejected: documenting the server as local-use-only. That is a rule with no
enforcement behind it. Instead paths exist only in osw.cli, and the operation
model refuses to register a path-like parameter on an MCP-surfaced operation, so
the server fails at import rather than shipping such a tool.

2. One server process per instance, no switching at runtime.
This reverses d393a66, which was agreed earlier in this PR. In-session
switching cannot keep the instance visible in the tool name, cannot make
read-only per instance, and is a prompt-injection target. Argument in full:
#133 (comment)

3. The server refuses to start unless an instance is named explicitly.
Rejected: failing at the first tool call, and inferring the sole iri of a
credential file. A server that cannot name its target would advertise tools that
all fail. The CLI still infers, because it resolves per invocation, reports what
it resolved, and --instance overrides any single command.

4. typer as a base dependency, not an extra.
Rejected: argparse plus a hand-written signature-to-parser translator (roughly 50
lines to own and keep honest), and click, which is decorator-per-option and would
mean writing every parameter twice. typer reads the same type hints and
docstrings the MCP SDK reads, which is what makes one declaration serve both. As
an extra it would let pip install osw ship a broken osw console script.

5. 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. It is included in osw[all] and in the dev group, so the server and its
tests share one environment with everything else. The earlier isolation, forced by
an anyio conflict with osw[workflow] and declared through [tool.uv] conflicts,
is gone: #139 is closed and
the pin is now anyio>=4.9,<4.14.

6. Canonical OSW_* configuration names, old names kept as aliases.
OSW_CRED_FILEPATH is already read by src/osw/express.py, so the previous
OSW_MCP_CRED_FILEPATH diverged from the library that owns the same setting.
OSW_MCP_* and OSL_* remain accepted, so existing deployments keep working.

Configuration

Required: an instance and credentials, either OSW_DOMAIN plus
OSW_USERNAME/OSW_PASSWORD, or OSW_DOMAIN plus OSW_CRED_FILEPATH.

.env handling differs by adapter on purpose. The CLI searches upward from the
working directory. The server searches nowhere, because its working directory is
chosen by the client, and takes its settings from the env block of its
registration. Both report the env file and credential file they resolved on
stderr before connecting.

Full reference, including registering one server per instance:
https://github.com/OpenSemanticLab/osw-python/blob/feat/mcp-server/docs/cli-and-mcp.md

Verification

uv sync
uv run python -m pytest tests/ -q     # 282 passed, 1 skipped
uv run ty check                       # All checks passed!
uv run deptry .                       # no dependency issues found

One environment covers everything. That includes the 17 MCP tests in
tests/test_mcp_registration.py, tests/test_mcp_server.py and
tests/test_no_paths_on_mcp_surface.py, which CI now runs alongside the rest.
They previously self-skipped in CI, because a plain uv sync --frozen never
installed the mcp extra, so the MCP code was effectively untested there.
tests/integration/test_mcp_server.py runs against a live instance.

Follow-ups

Filed while planning this work.

Resolved:

Open, deliberately not blocking this PR:

  • fetch_schema rewrites src/osw/model/entity.py as a side effect of ordinary read/write calls #141, fetch_schema
    rewriting the installed src/osw/model/entity.py as a side effect. This is
    pre-existing osw.core behaviour, but this PR adds two new entry points to it
    (create_or_update_entity, and export_entity_jsonld via autofetch_schema),
    so merging widens the exposure. The MCP get_category_schema tool is not
    affected, it reads the jsonschema slot directly. Documented in
    docs/cli-and-mcp.md; design discussion running on the issue.
  • MCP: ToolAnnotations silently accepts misspelled hints #142, ToolAnnotations
    silently ignoring a misspelled hint. The guard is already in this branch, typed
    Optional[bool] fields plus keyword construction plus a test; only the upstream
    report is outstanding.

The store_entity parallel-upload fix that once rode along on this branch is now
tracked in #132.

@LukasGold
LukasGold requested a review from simontaurus July 20, 2026 10:59
@LukasGold LukasGold self-assigned this Jul 20, 2026
@LukasGold LukasGold added the enhancement New feature or request label Jul 20, 2026
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.
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Release preview

Merging this PR would release v2.1.0 (current: v2.0.2).

Changelog preview (truncated)
## v2.1.0 (2026-08-31)

### Bug Fixes

- Allow uploading a file from an in-memory stream
  ([`64d99d6`](https://github.com/OpenSemanticLab/osw-python/commit/64d99d66ea45bc56bdaa6d12f2502df0bbbaf88c))

- **service**: Validate read_only via pydantic instead of truthy set
  ([`56711ed`](https://github.com/OpenSemanticLab/osw-python/commit/56711ed878b260288ba6100c9fdf2623544d43b7))

### Documentation

- Drop remaining references to the separate MCP environment
  ([`be75e47`](https://github.com/OpenSemanticLab/osw-python/commit/be75e47ced3ad4692f5b14a30d509d09c0a39c2e))

### Refactoring

- De-isolate the mcp extra from the dev environment
  ([`8f81fec`](https://github.com/OpenSemanticLab/osw-python/commit/8f81fec145fda977544f9d95576b7edf225c26a1))

- **service**: Validate Settings with pydantic
  ([`2051c26`](https://github.com/OpenSemanticLab/osw-python/commit/2051c26174e149455bf7111c0e8671d13259eddb))

### Testing

- Do not assume the first ask-query hit carries jsondata
  ([`f4ef72c`](https://github.com/OpenSemanticLab/osw-python/commit/f4ef72cdd2e73e42f01d36717f25b8239d1ab8c1))

- Rename oold.py to oold_test.py so its tests are collected
  ([`20072a9`](https://github.com/OpenSemanticLab/osw-python/commit/20072a9249cd97126a222c62a70f84e0433343ef))

- Stop test_init_from_env_vars leaking OSW_CRED_FILEPATH
  ([`4b7dd7e`](https://github.com/OpenSemanticLab/osw-python/commit/4b7dd7e5670eee4a230751412ad756153a9f2387))

Preview via python-semantic-release and conventional commits.

@LukasOro

Copy link
Copy Markdown

mcp>=1.2 resolves to 2.0.0, and the server no longer starts

Trying the branch out, the server dies before the MCP handshake:

File ".../osw/mcp/server.py", line 13, in <module>
    from mcp.server.fastmcp import FastMCP
ModuleNotFoundError: No module named 'mcp.server.fastmcp'

pyproject.toml declares mcp>=1.2, which currently resolves to mcp==2.0.0.
In 2.x the vendored FastMCP is gone: there is no mcp.server.fastmcp, no
FastMCP export from mcp.server, and no separate fastmcp package is pulled
in as a dependency. MCP clients surface this only as -32000: Connection closed, so it takes a manual run of osw-mcp to see the traceback.

Workaround for anyone hitting this right now:

uvx --from "osw[mcp] @ git+https://github.com/OpenSemanticLab/osw-python.git@feat/mcp-server" \
    --with "mcp<2" osw-mcp

Rather than capping at <2, I would suggest porting to the 2.x API. It looks
close to mechanical:

  • mcp.server.mcpserver.MCPServer, also re-exported as mcp.server.MCPServer,
    replaces FastMCP.
  • MCPServer.tool() keeps the same decorator-with-parentheses form, so the 15
    @mcp.tool() registrations in src/osw/mcp/tools/ stay as they are.
  • MCPServer.run() still defaults to transport="stdio".

In practice that is the import and the constructor in src/osw/mcp/server.py,
plus the -> FastMCP return annotations, 6 references in total.

Verified against mcp==2.0.0.

@LukasOro

Copy link
Copy Markdown

Credential loading bypasses the CredentialManager file pattern

config.load() requires domain, username and password from the environment
(OSW_DOMAIN / OSW_USERNAME / OSW_PASSWORD, with OSL_* fallbacks) and
raises RuntimeError if any of the three is missing. Failing fast rather than
reaching osw's interactive prompt is the right call for a stdio server, so this
is about which sources are accepted, not about the validation itself.

Deployments that authenticate through osw's own CredentialManager with a
credential file do not have those variables at all. Our FastAPI service
configures OSL as:

OSL_DOMAIN=osl.demo.open-semantic-lab.org
OSL_CRED_FILEPATH=/path/to/osl/cred/file.json

OSL_DOMAIN is picked up by the existing fallback, but there is no username or
password anywhere in the environment, so the server refuses to start. The only
way to run it today is to copy the credentials back out of the credential file
into a second plaintext .env that exists purely for the MCP server. That
duplicates the secret and adds another file to keep out of version control,
which is the situation CredentialManager is there to avoid.

Would you consider accepting a credential file as a source, for example reading
OSL_CRED_FILEPATH (or a dedicated OSW_MCP_CRED_FILEPATH) and passing it to
CredentialManager, and falling back to the current env-var path when it is
unset? That would let existing osw deployments point the server at the
credential store they already maintain.

@LukasOro

Copy link
Copy Markdown

Multi-instance credentials: selecting an OSL instance per session

Following on from the credential-file comment above. Pinning one .env at
registration assumes a user only ever talks to a single OSL instance, whereas
dev, staging and production wikis are usually all in play.

The multi-account file already exists. CredentialManager's credential file
is keyed by iri and already holds many accounts (save_credentials_to_file
writes data[cred.iri], iri_in_file(iri) looks one up). So this is less "add a
file format" and more "read the one osw already has, and let the caller choose an
entry".

One correction on the interactive part. Prompting on the CLI cannot work for
a stdio server: stdin and stdout are the JSON-RPC transport, so a prompt would
corrupt the stream, and there is no TTY. That is what config.load()'s fail-fast
protects against today, and CredentialFallback.ask would hang the server for
the same reason. The MCP-native equivalent is elicitation, which the 2.x SDK
exposes (Elicit, AcceptedElicitation, DeclinedElicitation in
mcp.server.mcpserver). Another argument for the port suggested above.

Elicit the choice, never the secret. Elicited values pass through the MCP
client and into the agent's context. Selecting an iri by name is fine; entering a
password there is not. Passwords should stay in the credential file and never
transit MCP.

Concretely:

  • list_instances() returns the iris in the credential file, no secrets.
  • select_instance(iri) sets the active instance, calls connection.reset() so
    the cached OswExpress is rebuilt, and re-creates the Ledger. The ledger is
    already domain-scoped via _safe_domain(domain), so provenance stays separated.
  • status() reports the active iri. With several instances reachable that stops
    being a nicety.
  • Auto-select when unambiguous: if OSW_DOMAIN is set, or the file holds exactly
    one iri, skip the prompt.
  • If nothing is selected, tools should fail with "no instance selected, call
    select_instance first" rather than the server refusing to start. That does
    invert today's startup validation, so it is worth a deliberate decision.

Multiple registrations already cover part of this, with no code change:

claude mcp add osw-dev  --env OSW_MCP_ENV_FILE=... -- uvx ...
claude mcp add osw-prod --env OSW_MCP_ENV_FILE=... --env OSW_MCP_READ_ONLY=true -- uvx ...

That has two advantages over in-session switching: the instance is visible in the
tool name at every call site, and read-only can be set per instance, so production
stays read-only while dev is writable. In-session selection should preserve both,
ideally with a per-iri read-only setting.

Supplying a .env path at runtime still works as a fallback for one-off instances
not in the credential file, though it is the weakest option since it puts secrets
on disk in a second place.

@LukasGold LukasGold changed the title feat/mcp-server feat(mcp): add osw-mcp server exposing a live OSL instance Aug 22, 2026
- 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
- 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
- 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
@LukasGold

LukasGold commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

All three addressed, one commit each: e219aae, a7c35a7, d393a66.

mcp>=1.2 resolves to 2.0.0

Confirmed against the published wheel: mcp/server/fastmcp/ is gone in 2.0.0 and
MCPServer is exported from mcp.server. Ported to 2.x rather than pinned to
<2; the decorator and run() surface are unchanged.

The anyio clash you predicted is real, and it is a constraint the PR now has to
carry rather than solve: mcp>=2 wants anyio>=4.9, osw[workflow] pins
anyio<4.7 for prefect 2.20.25. The workflow pin stays the default resolution,
mcp is declared exclusive with workflow and dev under [tool.uv] conflicts,
and the pytest stack moved into its own test group so an environment holding
both pytest and mcp can exist at all.

Cost: the module is neither type-checked nor unit-tested in the default
environment, and CI does not run the mcp one.
#139 is the reminder to
re-check the pin.

Credential loading bypasses the CredentialManager file pattern

An iri-keyed credential file now configures the server, built into a
CredentialManager and passed to OswExpress. Validated at startup: the file
must exist and hold an entry for the configured domain, with the error naming the
iris it does contain.

Two things worth confirming:

  • osw itself does not read OSL_CRED_FILEPATH. CredentialManager takes
    cred_filepath as a constructor argument only, so that variable is your service
    convention, not a library one. Kept as a fallback name, since deployments set it.
  • Lookups use CredentialFallback.none, so osw never reaches its interactive
    prompt. On stdio a prompt would corrupt the JSON-RPC stream and hang the server.
    Nothing calls save_credentials_to_file(); the server still never writes
    credentials to disk.

Multi-instance credentials

Agreed on the constraint: elicited values pass through the client and into the
agent's context, so instance selection is by name only and no secret transits MCP.

Implemented as list_instances (iris only) and select_instance(iri), which
rebuilds the connection and the per-domain ledger. OSW_DOMAIN becomes optional
when a credential file supplies the iris, auto-selecting when it is set or when the
file holds exactly one, so single-instance setups are unchanged.

Note this piece was reversed later in the PR, see
#133 (comment).

- 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
- 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
- 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
- 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
- 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
- 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
@LukasGold

LukasGold commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Reversing in-session instance switching (d393a66)

list_instances and select_instance are removed in cb7d807. Each server
process is now pinned to one instance for its lifetime and refuses to start if
none is configured. Multi-instance support becomes one registration per instance.

This is a reversal of what we agreed, so the reasoning in full.

It cannot deliver the two advantages you named for
multiple registrations.

  • Instance visible in the tool name. With in-session selection every call is
    mcp__osw__get_entity whichever wiki it targets, so the permission prompt
    cannot show the destination. That visibility is a property of separate servers;
    switching cannot carry it.
  • Read-only per instance. Write tools are filtered at registration, so a model
    never sees a tool it cannot use. Per-iri read-only would mean the registered
    tool set has to change after the list has been advertised. The alternatives are
    registering write tools always and failing at call time, or a
    tools/list_changed round trip.

A threat model that was not in the original discussion. Wiki content an agent
reads is untrusted input, so a tool that moves the session from dev to prod is a
prompt-injection target: injected text on a page can ask for
select_instance("prod") and then write there. Under static pinning, prod is a
separate process with separate credentials, its own ledger and its own permission
rules.

Your point 5, on startup validation, taken deliberately. The server fails at
startup rather than returning "no instance selected" at call time, so a
misconfigured server never advertises tools that would all fail.

Unchanged: the iri-keyed CredentialManager file, status reporting the
active iri, and no secret transiting MCP. Elicitation is no longer needed here,
which drops that dependency.

Where switching went: the CLI added in c1cede3. osw --instance <iri> and
osw instance list are stateless per invocation, and the target is visible in the
command line and in shell history.

- 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
- drop src/osw/mcp from [tool.ty.src] exclude
- silence only the two unresolvable SDK imports inline (issue #139)
- 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
- 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
- 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
- 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
- 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
- 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
- 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
@LukasGold LukasGold changed the title feat(mcp): add osw-mcp server exposing a live OSL instance feat: add an osw CLI and MCP server on a shared service core Aug 28, 2026
@LukasGold
LukasGold marked this pull request as ready for review August 28, 2026 09:37
@LukasGold

LukasGold commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review

The description no longer matched the branch and has been rewritten around what
the PR delivers and which decisions need agreement.

Four things now differ from what was reviewed in the first round:

Change Effect on the surface
No runtime instance switching select_instance, list_instances gone; one registration per instance (reasoning)
No filesystem path on the MCP surface download_file, upload_file become osw file download/upload; MCP gets get_file_info, read_file_text, write_file_text
Canonical OSW_* configuration names OSW_MCP_* and OSL_* still accepted, so nothing breaks
An osw command line client new, covers everything the MCP server does plus the path-based operations

Two behaviours settled since then:

  • The server requires an instance to be named explicitly and refuses to start
    otherwise. It never infers one, not even from a credential file holding a single
    iri. The CLI still infers, because it resolves per invocation, reports what it
    resolved, and --instance overrides any single command.
  • .env handling now differs by adapter on purpose: the CLI searches upward from
    the working directory, the server searches nowhere and reads its env block.
    Both report the files they resolved on stderr before connecting.

The CLI and MCP documentation moved out of the README into
https://github.com/OpenSemanticLab/osw-python/blob/feat/mcp-server/docs/cli-and-mcp.md.

Default suite is 236 passed, 4 skipped, with ty, deptry and ruff clean. The
three files importing the MCP SDK need their own environment, commands are in the
description; CI does not run it yet.

- --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
@LukasGold LukasGold changed the title feat: add an osw CLI and MCP server on a shared service core feat: an osw CLI and an MCP server for live OSL instances Aug 28, 2026
Resolve the anyio conflict that forced the mcp isolation:
- main widened the workflow pin to anyio>=4.9,<4.14, compatible with mcp>=2
- drop [tool.uv] conflicts and the separate test dependency group
- add mcp to the all extra and to the dev group
- relock
- 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
- 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
Closes #140.

- express.py: replace the unreachable isinstance(source, IO) check with a
  duck-typed one; typing.IO is not runtime-checkable
- InMemoryController: drop the __init__ that assigned stream before the
  model was initialised and overwrote a caller-supplied stream
- default the stream to BytesIO, matching the byte-oriented get/put
- declare IO in the upload_file / osw_upload_file signatures
- 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
- 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
- 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
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants