* A contribution can be extended
A plugin offers something — a view, a command, a tool — and another plugin
wants to add to *that* one rather than offer a rival. Its only lever was to
contribute a replacement and hope to be read last, which makes the outcome
depend on the order plugins happened to load in: it works on the machine
where the names sort the right way and nowhere else.
An extension is an ordinary contribution to a derived point, so order,
identity, tenant filtering and disposal with the plugin all hold for
extensions too, and a host reads them without a second mechanism.
* reactor_mcp_server 1.0.0: an extensible MCP server
Three ideas. A tool is a contribution, so what a server is made of can be
read before one is built. A contribution can be extended, so an extension
narrows somebody else's tool by name and in a declared order instead of
registering a rival and hoping to load second. And the URL decides what is
served: tools belong to named toolsets, a client asks for them with
`/mcp?benchmarks`, and an extension nobody asked for is not registered at
all — reactor holds it on an activation event until a URL names its toolset.
Tested against a real MCP client over HTTP, which is where the two things a
design like this gets wrong turned up: a mounted sub-application answers a
redirect the protocol does not follow, and one whose lifespan nobody runs
answers 'Task group is not initialized' to everything.
* reactor_mcp_server: an extension may act on the built server
The escape hatch the Datalayer gateway needs, and deliberately a narrow one:
everything an extension offers goes through the contribution points, where a
host can read it without running anything, and `on_server` is for what an
extension has to do *to* a server — take off a tool a scaffold registered
outside the contribution model, add what the SDK exposes and a spec does not
describe.
Called after the tools are on, per built server, so an extension whose
toolset is not active is not called; one that raises costs only itself.
* reactor_mcp_server 1.0.1: what a deployment decides
Three seams a host that serves over HTTP needs, found by putting the Datalayer
MCP gateway on this.
**The server a selection is served by is the deployment's to make.**
`McpHost.build` made a bare `MCPServer`; the gateway's carries CORS, an
identity middleware and the token verifier the operator configured, and a
client reaching a toolset through a bare one would reach it unauthenticated.
`server_factory` is how a host says what to build.
**So are the transport options.** `stateless_http`, the body limit, the
transport settings — the SDK takes them where the application is built, which
the router does. A deployment that needs sessions was getting the SDK's
default instead, and a client waiting for an `Mcp-Session-Id` was handed
nothing. `app_options` are passed to every selection's application: another
toolset is not another transport.
**A lifespan gets a task of its own.** It was opened inline in the request
that first asked for a set of toolsets and closed on shutdown, which is a
cancel scope entered in one task and left in another: anyio raises at the end
— "Attempted to exit cancel scope in a different task than it was entered in"
— so a worker's last words were "Application shutdown failed", after the work
was done and where nobody is looking.
And, from the same migration: a tool that names no toolset goes in its
extension's **first declared** one rather than one named after the plugin. An
extension declaring `sandboxes` under the name `sandboxes-datalayer` put its
tools in a toolset nobody declared, and a toolset nobody declared is never
active — 35 tools contributed, 14 served, nothing saying why.
* reactor_mcp_server 1.0.2: a toolset name is a subject, not a plugin
Two extensions adding to the same toolset declare the same name — which is
how a deployment puts its own sandbox tools in `sandboxes` beside the ones
that ship, and how `/toolsets` came to list `sandboxes` twice, with two
descriptions, to a client asking what there is.
The name identifies it. The first declaration is the one described; the rest
add their tools and say so at debug.
* Document the MCP server foundation
Six pages under a section of its own, because `reactor_mcp_server` is the one
plugin package that is also a product: a deployment installs a distribution
and its tools are served, which is worth explaining as its own subject rather
than as a paragraph in the Python runtime.
- **Tools** — offering one, what a `ToolSpec` carries, where the description
comes from, and putting them on a server by hand.
- **Extending a tool** — what replaced registering the same name and hoping to
load second, what a `ToolExtension` may change, the order they compose in,
and why extending a tool nobody offered is not an error.
- **Toolsets** — the URL grammar, `always` and `default`, activation events,
and that a toolset name is a subject rather than a package.
- **The host** — building a server, what a `BuiltServer` carries, the server
factory and `on_server`, and what one misbehaving plugin costs.
- **Serving** — the three routes, running the installed extensions, transport
options, and the two things a host of the SDK's app must get right.
- **Writing an extension** — class to published distribution, with the
checklist.
Every claim about the API is checked against the code, and each example is
run rather than written from memory. Six mermaid diagrams, all of which parse
and draw; the site builds with no broken links.
* toolsets
* Depend on the released Datalayer packages
Version floors only: @datalayer/primer-addons 1.0.23, @datalayer/core 1.2.12, @datalayer/agent-runtimes 1.3.13, @datalayer/jupyter-react, jupyter-lexical, jupyter-embed and jupyter-docusaurus-plugin 2.0.17; no code change.
* CI installs the MCP server plugin, whose tests run with the rest
* CI: the MCP plugin's async tests run under asyncio_mode auto with its test extra
* 1.0.3: one version for every package; release.yaml publishes them all on a tag, trusted publishing
- @datalayer/reactor, reactor-commands, reactor-graph, reactor-manager and
reactor-shell at 1.0.3, the plugins' floor on @datalayer/reactor with it;
datalayer-reactor follows package.json; reactor-mcp-server 1.0.3 with its
floor on datalayer_reactor raised.
- release.yaml (was release.yml): on a v* tag, checks the tag names every
package's version, builds and tests, packs and publishes the five npm
packages (root first) through the npm environment and the two Python
packages through the pypi environment — OIDC, no token — skipping what
is already on the registries; then a GitHub release.
- ci.yml and fix-license-header.yml renamed to .yaml.
- RELEASE.md: the packages, the trusted publisher settings, the steps.
* RELEASE.md formatted
* plugins keep their floor on a published @datalayer/reactor
The root package is not a workspace member, so npm resolves it from the
registry: a floor on the version being released cannot install (ETARGET).
* MCP server: what the review found
- The router follows the host's revision: an extension added or the cache
dropped means new applications for new connections, not the stale ones.
- on_reactor_start / on_reactor_stop reach on_start / on_stop (pluggy
hookimpl); the platform never called the latter.
- Tools without a declared toolset get one declared, named after the
extension, so they can be selected at all.
- A tenant is a dimension of every read and of the cache key; the router
takes a tenant resolver.
- Resources, prompts and on_server belong to the selection like tools do:
an extension that is not participating puts nothing on the server.
- A held extension's toolsets are declared before anybody asks for them, so
/toolsets can name them.
- A wrapper keeps the tool's docstring description; the manifest declares
the resources and prompts points; __version__ is the distribution's.
- reactor core: reading a contribution's extensions wakes a plugin held on
the derived point.
- cms-astro example: jupyter-lexical ^2.0.17.
Tests for each, in tests/test_review_follow_ups.py.