Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 107 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ 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/).
Note that `osw[mcp]` is not part of `osw[all]` and has to be installed
explicitly, see [MCP server](#mcp-server).

## Quickstart

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

## 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 upload/download files.

```bash
pip install "osw[mcp]"
```

This extra is deliberately not part of `osw[all]`. It needs `anyio>=4.9`, which
conflicts with the pin the `osw[workflow]` extra requires for prefect 2.x, so
the two cannot share an environment
([#139](https://github.com/OpenSemanticLab/osw-python/issues/139)). Installing
the server standalone, for example via `uvx`, avoids the question entirely.

Configure credentials in a gitignored `.env` file (the server reads them at
startup and never writes them 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_MCP_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_MCP_CRED_FILEPATH=/abs/path/to/accounts.pwd.yaml
```

`OSL_CRED_FILEPATH` is accepted as a fallback, so deployments that already
configure osw's `CredentialManager` need no extra setup. The file is the YAML
format `CredentialManager` already reads, keyed by iri:

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

**Multiple instances:** when the credential file holds more than one iri, the
server starts without an active instance and exposes two extra tools:

- `list_instances` returns the available iris, never any credential
- `select_instance(iri)` switches to one, rebuilding the connection and the
provenance ledger, which is kept separate per domain

If `OSW_DOMAIN` is set, or the file holds exactly one iri, that instance is
selected automatically and neither tool needs to be called. Until an instance is
active the other tools return "No OSL instance selected". `status` reports which
one is active.

Registering the server once per instance works too, and has the advantage that
the instance is visible in the tool name at every call site, with read-only
settable per instance:

```bash
claude mcp add osw-dev --env OSW_MCP_ENV_FILE=/abs/path/dev.env -- uvx --from "osw[mcp]" osw-mcp
claude mcp add osw-prod --env OSW_MCP_ENV_FILE=/abs/path/prod.env --env OSW_MCP_READ_ONLY=true -- uvx --from "osw[mcp]" osw-mcp
```

Register it with Claude Code (reference the `.env` via `OSW_MCP_ENV_FILE`; do
not put `OSW_PASSWORD` inline in a committed `.mcp.json`):

```json
{
"mcpServers": {
"osw": {
"command": "uvx",
"args": ["--from", "osw[mcp]", "osw-mcp"],
"env": { "OSW_MCP_ENV_FILE": "/abs/path/to/.env" }
}
}
}
```

Or via the CLI:

```bash
claude mcp add osw --env OSW_MCP_ENV_FILE=/abs/path/to/.env -- uvx --from "osw[mcp]" osw-mcp
```

**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`.

**Editable-checkout caveat:** `create_or_update_entity` and
`export_entity_jsonld` call `fetch_schema`, which regenerates
`src/osw/model/entity.py` inside the installed package. With a normal
`pip install "osw[mcp]"` this writes into site-packages and is harmless. If you
run the server from an editable source checkout, those two tools will modify the
generated model file in your working tree. The read tools (`get_entity`,
`get_slot`, `get_category_schema`, ...) read raw page slots and never trigger
this.

## Contributing

Contributions are welcome, see [CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
57 changes: 52 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ dataimport = [
"openpyxl",
]
UI = ["pysimplegui"]
mcp = [
# official MCP Python SDK; provides MCPServer from mcp.server.
# requires 2.x: 1.x has no MCPServer, and 2.0 removed the vendored FastMCP.
"mcp>=2",
# .env loading for the stdio server (OSW_DOMAIN/OSW_USERNAME/OSW_PASSWORD)
"python-dotenv>=1.0",
]
workflow = [
"prefect>=2.20.25,<3.0",
# prefect 2.20.25 is the final 2.x release (no backports). Its
Expand All @@ -79,22 +86,41 @@ workflow = [
"anyio>=4.4.0,<4.7",
]
tutorial = ["osw[dataimport]"]
# mcp is deliberately excluded here: it requires anyio>=4.9, which conflicts
# with the workflow extra's anyio cap. Install it explicitly with osw[mcp].
# See https://github.com/OpenSemanticLab/osw-python/issues/139
all = ["osw[dataimport,DB,UI,S3,wikitext]"]

[project.scripts]
# stdio MCP server exposing a live OSL instance to MCP clients (e.g. Claude Code)
osw-mcp = "osw.mcp.server:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[dependency-groups]
dev = [
# test stack
# pytest stack in its own group so it can be installed alongside the mcp
# extra, which conflicts with the dev group (see [tool.uv] below).
# Run the MCP tests with:
# uv sync --extra mcp --group test --no-dev
# uv run --extra mcp --group test --no-dev --no-sync python -m pytest tests/test_mcp_*.py
test = [
"pytest",
"pytest-cov",
"pytest-mock",
"pytest-asyncio",
# inherit the capped prefect pin (<3.0); a bare "prefect" here resolved to
# 3.x in CI, whose server API breaks the prefect-2.20-targeted tests
"osw[workflow]",
]
dev = [
{ include-group = "test" },
# prefect/anyio are listed directly rather than via osw[workflow]: the
# workflow extra is in a uv conflict set (see [tool.uv] below), so a
# self-referential osw[workflow] entry here would only activate when
# --extra workflow is passed, leaving a bare `uv sync` on the anyio that
# breaks prefect 2.20. Keep these pins in sync with the workflow extra.
# Tracked in https://github.com/OpenSemanticLab/osw-python/issues/139
"prefect>=2.20.25,<3.0",
"anyio>=4.4.0,<4.7",
"geopy",
"deepl",
"sqlalchemy",
Expand Down Expand Up @@ -291,6 +317,19 @@ insertion_flag = "<!-- version list -->"
[tool.semantic_release.changelog.default_templates]
changelog_file = "CHANGELOG.md"

[tool.uv]
# mcp 2.x needs anyio>=4.9; the workflow extra caps anyio<4.7 for prefect 2.20
# (see the workflow extra above). They cannot share one resolution, so uv is
# told to resolve them in separate splits. Install the MCP server standalone:
# pip install "osw[mcp]".
# The dev group is included too since it carries the same anyio cap directly
# (see the dev group above). Tracked in
# https://github.com/OpenSemanticLab/osw-python/issues/139
conflicts = [
[{ extra = "mcp" }, { extra = "workflow" }],
[{ extra = "mcp" }, { group = "dev" }],
]

[tool.ty.environment]
python = "./.venv"
python-version = "3.10"
Expand All @@ -300,12 +339,17 @@ python-version = "3.10"
# - src/osw/model/entity.py: generated (datamodel-code-generator) models
# - examples, scripts: illustrative/maintenance code, not part of the package
# - tests: not yet type-clean, tightened in a follow-up
# - src/osw/mcp: its dependencies (the mcp extra) cannot be installed
# alongside the workflow extra (see [tool.uv] conflicts); revert this
# once the anyio conflict is resolved
# (https://github.com/OpenSemanticLab/osw-python/issues/139)
exclude = [
"src/osw/model/entity.py",
"examples",
"scripts",
"tests",
"docs",
"src/osw/mcp",
]

[tool.ty.rules]
Expand Down Expand Up @@ -343,6 +387,9 @@ pybars3-wheel = "pybars"
psycopg2 = "psycopg2"
openpyxl = "openpyxl"
pysimplegui = "PySimpleGUI"
mcp = "mcp"
# python-dotenv imports as `dotenv`
python-dotenv = "dotenv"

[tool.deptry.per_rule_ignores]
# DEP002: declared but not imported anywhere in src
Expand Down
21 changes: 21 additions & 0 deletions src/osw/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""osw-mcp: an MCP server exposing a live OpenSemanticLab instance.

The server wraps :class:`osw.express.OswExpress` and serves it over the Model
Context Protocol (stdio) so MCP clients such as Claude Code can search, read,
write and manage entities, page slots and files on a live OSL instance.

``main`` is imported lazily so ``import osw.mcp`` does not require the optional
``mcp`` / ``python-dotenv`` dependencies unless the server is actually started.
"""

from __future__ import annotations

__all__ = ["main"]


def __getattr__(name: str):
if name == "main":
from .server import main

return main
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
8 changes: 8 additions & 0 deletions src/osw/mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Allow ``python -m osw.mcp`` to launch the server."""

from __future__ import annotations

from .server import main

if __name__ == "__main__":
main()
Loading
Loading