Repository navigation
Move reactor-mcp-server to apps/, make toolsets first class, add example toolsets - #26
Conversation
…ment runs, not a piece an application installs
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Plugin activation can leave stale servers and skip lifecycle startup, while CLI shutdown omits extension cleanup.
Review effort: Balanced
Findings: None
What changed in this PR
Moves the MCP server from plugins/ to apps/, reflecting its role as a deployable host while preserving package metadata.
Changes:
- Relocates MCP server implementation, tests, and documentation.
- Updates CI, release, and version-bump paths.
- Documents the new
apps/directory purpose.
| File | Description |
|---|---|
RELEASE.md |
Updates release package path. |
dev/bump_version.py |
Updates MCP version-file paths. |
apps/README.md |
Defines the applications directory. |
apps/mcp-server/.gitignore |
Ignores package build metadata. |
apps/mcp-server/README.md |
Documents MCP server usage. |
apps/mcp-server/pyproject.toml |
Defines package metadata and dependencies. |
apps/mcp-server/reactor_mcp_server/__init__.py |
Exposes the public API. |
apps/mcp-server/reactor_mcp_server/__main__.py |
Provides the server CLI. |
apps/mcp-server/reactor_mcp_server/app.py |
Implements HTTP routing and lifespans. |
apps/mcp-server/reactor_mcp_server/extension.py |
Defines MCP extensions and discovery. |
apps/mcp-server/reactor_mcp_server/points.py |
Defines contribution points and events. |
apps/mcp-server/reactor_mcp_server/server.py |
Builds and caches MCP servers. |
apps/mcp-server/reactor_mcp_server/tools.py |
Defines tool contributions and extensions. |
apps/mcp-server/reactor_mcp_server/toolsets.py |
Parses and resolves toolset selections. |
apps/mcp-server/tests/test_activation_by_url.py |
Tests URL-based activation. |
apps/mcp-server/tests/test_review_follow_ups.py |
Covers prior review regressions. |
apps/mcp-server/tests/test_served_over_http.py |
Tests served HTTP behavior. |
apps/mcp-server/tests/test_tools_are_contributions.py |
Tests contribution composition. |
.github/workflows/ci.yaml |
Installs tests from the new path. |
.github/workflows/release.yaml |
Builds and releases from the new path. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ment's default selection and own query keys, host.active_for, stdio and --toolsets on the CLI; math and geo example toolsets
…servers when a URL wakes an extension; stop the host on CLI exit; list_server_toolsets, never shadowing an extension's tool
|
Addressed the review overview's three points in a21227c:
Also renamed the optional tool to Tests: 239 passed and 1 skipped at the root; with the example installed, its 12 pass, stdio included. |
…cp_toolsets_example)
…n/rsbuild-plugin ^2.9.0, as decks does, on @rsbuild/core 2: one resolution for the workspace instead of a 0.20 plugin wanting @rsbuild/core 1
…bject / unregister_extension (rescan uses them), has_plugin, EXTENSION_CONTENT_TYPES, hookimpl exported; a route descriptor may carry its endpoint, mounted by include_plugin_routes and described without it (describe_routes, /tenants/{id}/routes)
Python `reactor.ui`: the envelope {v:1, op, scope, id, body} with eight ops
(show|patch|append|remove|state|ask|window|meta) as pydantic models; Display
(typed fields as `data`, mandatory text(), model_view()); @display_type with
MIME-like types and versions; audiences (`both`, `page`, secrets for neither);
ActionRegistry (HMAC tokens bound to session + display + action, unknown
action 422, bad token / veto 403, idempotency ids); UiChannel and
MemoryUiChannel; an optional AG-UI encoder; the JSON Schema, committed as
schema/reactor-ui.v1.schema.json and written by `reactor ui schema`. Hooks
provide_display_types and authorize_ui_action (any False or a raise vetoes).
TypeScript `@datalayer/reactor/ui`: wire types held to the schema by a drift
test, the Renderers and ToolBindings contribution points, resolveRenderer
(rank, then registration order, version range), resolveToolResult (embedded
_meta["reactor.ui/display"] first, then a binding), and a pure reducer with
RFC 6902. `@datalayer/reactor/ui/react`: ReactorDisplay (lazy renderer, text
fallback) and useUiChannel.
Docs: docs/docs/ui-channel. Every package bumped to 1.1.0.
- actions: the token's HMAC message length-prefixes each part (8-byte big-endian length + bytes), so one token binds exactly one tuple. - actions: a running press is never forgotten; only settled ones make room, and a press arriving while every remembered press runs is refused with UiActionBusy (429). - display types: each provider declares into its own registry, merged only once it succeeded (all-or-nothing, conflicts included). - agui: move/copy `from` is rebased under /display with `path`. - wire: a patch operation is an op-discriminated union (PatchAdd, PatchRemove, PatchReplace, PatchMove, PatchCopy, PatchTest) with the members RFC 6902 requires; schema regenerated, TS types and drift fields follow. - reducer: pointers walk own properties only and refuse __proto__, constructor and prototype; `test` compares structurally, not by key order. - wire.ts: isUiOp checks each op's id and body as the Python models do. - renderers: textOf falls back to String() when JSON.stringify has no answer.
… (CI installed .[dev] without the agui extra)
…s the operations, 0.1 keeps dicts)
…tocol 0.1.19 and 1.0.0)
…e (PR #27) docs/docs/ui-channel: an overview (one wire, displays with a text and what a model sees, renderers keyed by type, actions with tokens) and a page each for displays and audiences, renderers and tool bindings, the channel ops and the reducer (JSON Patch rules, pointer safety), actions (tokens, the veto hook, idempotency, 403/422/429), AG-UI, the schema and types, and security. Linked from the docs index, both getting-started pages and the examples index. examples/ui-channel: an approval card from a FastAPI host to a React page and back -- a @display_type with page-only fields and a secret, a plugin that declares the type and vetoes a requester deciding their own request, the ops a decision emits, and a page whose plugin contributes a lazy renderer and a tool binding, keeps the ops with useUiChannel and presses with the offered token. A recorded session (fixtures/demo-ops.json) is checked by the Python test and replayed by the vitest tests, which the root vitest config now includes.
…TOR_UI RU-2) — ACTIVITY_SNAPSHOT/DELTA and CUSTOM reactor.ui back into ops, scope kept per display, anything of the reactor's not its shape refused; one committed example stream holds encoder and decoder together
…b (a UiChannel: ops numbered and kept per scope, handed to every connection on it) and UiSocket (the ASGI endpoint a host mounts): hello first with the credential (never in the URL), welcome, ops replayed after since then live, action and state frames answered by result frames with the registry's status, pings both ways, a bounded queue per connection (4429), every frame checked against the schema (4400), 4401/4403/4408/4410; the frames in reactor.ui.wire and the committed schema. Page: @datalayer/reactor/ui/ws UiSocket (ordering, ops applied once, gap and drop resume from the last op, presses sent again with their idempotency id, refusal stops, heartbeats) and useUiSocket in ui/react. One committed exchange (schema/reactor-ui.v1.ws-examples.json) written by tests/test_ui_ws.py and replayed by src/ui/__tests__/ws.test.ts; two tabs on both tiers. The approval card example moves from polling and an action POST to the socket. Docs ui-channel/websocket
…sh hello, up to the welcome's seq) — drawn again, never acted on as news; useUiSocket raises onWindow for new window ops only (REACTOR_UI RU-W)
…both tiers (REACTOR_UI P5, RU-4): what a display shown by ask offers, the host checking the value against what it asked; docs ui-channel/actions, the page pressing up the socket
….1 and 1.0 order a JSON patch's keys differently (CI on 1.0.0)
…RU-4) and AguiUiDecoder (RU-2) on top of 1.1.0's ui channel
reactor.ui 1.1.0: one wire between a Python host and the page (RU-0)
… keeps a file by scope and ref for its owner (25 MB a file, 256 MB in all, oldest out), its url signed and short-lived; UiFilesRoutes, the upload (bytes as the body, scope and name in headers, the credential checked by the host's authenticate) and the signed download a host mounts (400/401/403/404/413); TS uploadFile, isFileRef, UiUpload, UiFileRefused; a renderer's upload prop beside act; docs ui-channel/files. pytest test_ui_files 4, vitest files 2
Move
plugins/mcp-server→apps/mcp-server. The MCP server is a host a deployment runs, not a plugin an application installs.PY_PACKAGES),dev/bump_version.pyandRELEASE.mdfollow the new path.apps/README.mdsays what the folder is for.Toolsets, first class
The generic parts of what the private Datalayer gateway needed, moved into the foundation:
Toolset.titleandToolset.instructions. A toolset's instructions go into the server's instructions only when it is active (host.instructions_for).list_toolsetstool (build_host(..., list_toolsets_tool=True)). It gives the same answer as/toolsets, for a model already in a session or a stdio client. Read-only, and off by default in code.default_selectiononcreate_mcp_app/ToolsetRouter: what a URL naming no toolset gets.ignore_query_keys: query keys the deployment reads itself (e.g.scopes=), which are no longer taken for unknown toolset names.parse_selectionandselection_from_scopetakeignore=.host.active_for(selection): what a request is about to get, without building. It answers correctly foronly=, defaults andwithout=, where checking the named flags does not.host.describe(selection): the one payload behind/toolsetsandlist_toolsets. It now includes each toolset'stitle.--transport stdio,--toolsets(the default selection, and over stdio the whole selection),--ignore-query-key, and--list-toolsets-tool(on by default).The gateway's in-session enable/disable was not ported on purpose: toolsets are read once per connection, as the docs explain.
Example
examples/toolsetsis one installable distribution with two extensions:math(add,multiply) is on by default and has instructions.geo(get_capital_of_country,get_country_of_capital) is opt-in with anonToolset:geoactivation event.It is tested in process, over HTTP with a real client, and over stdio through the entry points. CI installs it.
Docs
toolsets.md: instructions,list_toolsets, what a deployment decides, andactive_for.serving.md: the new CLI options and stdio.host.mdandindex.mdare updated.example-toolsets.md.Tests: 231 passed from the root (80 in the app, 12 in the example).