Skip to content

Move reactor-mcp-server to apps/, make toolsets first class, add example toolsets - #26

Merged
echarles merged 26 commits into
mainfrom
feat/apps-mcp-server
Oct 17, 2026
Merged

echarles merged 26 commits into
mainfrom
feat/apps-mcp-server

Conversation

@echarles

@echarles echarles commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Move

plugins/mcp-server → apps/mcp-server. The MCP server is a host a deployment runs, not a plugin an application installs.

  • CI, the release workflow (PY_PACKAGES), dev/bump_version.py and RELEASE.md follow the new path.
  • apps/README.md says what the folder is for.

Toolsets, first class

The generic parts of what the private Datalayer gateway needed, moved into the foundation:

  • Toolset.title and Toolset.instructions. A toolset's instructions go into the server's instructions only when it is active (host.instructions_for).
  • list_toolsets tool (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_selection on create_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_selection and selection_from_scope take ignore=.
  • host.active_for(selection): what a request is about to get, without building. It answers correctly for only=, defaults and without=, where checking the named flags does not.
  • host.describe(selection): the one payload behind /toolsets and list_toolsets. It now includes each toolset's title.
  • CLI: --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/toolsets is 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 an onToolset:geo activation 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, and active_for.
  • serving.md: the new CLI options and stdio.
  • host.md and index.md are updated.
  • New page: example-toolsets.md.

Tests: 231 passed from the root (80 in the app, 12 in the example).

…ment runs, not a piece an application installs
Copilot AI balanced review requested due to automatic review settings September 29, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@echarles echarles changed the title Move reactor-mcp-server to apps/ Move reactor-mcp-server to apps/, make toolsets first class, add example toolsets Sep 29, 2026
…servers when a URL wakes an extension; stop the host on CLI exit; list_server_toolsets, never shadowing an extension's tool
@echarles

Copy link
Copy Markdown
Member Author

Addressed the review overview's three points in a21227c:

  • Lifecycle startup skipped. PluginPlatform.activate_plugin now delivers on_reactor_start to a plugin that activates after start(), whether an event woke it or it was registered or discovered late. It is delivered to that plugin only, and once. A plugin stood down while the platform runs gets on_reactor_stop at that point. The separate late-start path for discovered extensions is folded into this, so nothing is started twice. Tests are in tests/test_start_hooks.py.
  • Stale servers. McpHost.build drops the cached servers (forget_built, which bumps revision) whenever the events it fires wake or stand down a plugin. The router then makes new applications for new connections. Tests are in TestWakingAnExtension.
  • CLI shutdown. reactor-mcp-server now stops the host when either transport exits, so every running extension gets on_stop.

Also renamed the optional tool to list_server_toolsets (host option toolsets_tool, CLI --toolsets-tool) so it cannot collide with a deployment's own list_toolsets. An extension offering a tool of that name is served instead of the host's.

Tests: 239 passed and 1 skipped at the root; with the example installed, its 12 pass, stdio included.

…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)
…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)
echarles and others added 2 commits October 12, 2026 14:00
…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
@echarles
echarles merged commit f2d7e09 into main Oct 17, 2026
9 checks passed

This branch was successfully deployed

2 active deployments
npm — c104a542 Deployed Oct 12, 2026 by echarles via npm #6
pypi — c104a542 Deployed Oct 12, 2026 by echarles via pypi #6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants