reactor-mcp-server 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ *.egg-info/
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.5
2
+ Name: reactor_mcp_server
3
+ Version: 1.0.1
4
+ Summary: An extensible MCP server built on Reactor: tools are contributions, and a contribution can be extended
5
+ Project-URL: Homepage, https://datalayer.ai
6
+ Project-URL: Source, https://github.com/datalayer/reactor
7
+ Author-email: Datalayer <support@datalayer.io>
8
+ License: Datalayer License
9
+ Keywords: mcp,plugins,reactor,tools
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: datalayer-reactor>=1.0.2
18
+ Requires-Dist: mcp<3,>=2
19
+ Requires-Dist: starlette
20
+ Provides-Extra: server
21
+ Requires-Dist: uvicorn[standard]; extra == 'server'
22
+ Provides-Extra: test
23
+ Requires-Dist: httpx; extra == 'test'
24
+ Requires-Dist: pytest; extra == 'test'
25
+ Requires-Dist: pytest-asyncio; extra == 'test'
26
+ Requires-Dist: uvicorn[standard]; extra == 'test'
27
+ Description-Content-Type: text/markdown
28
+
29
+ <!--
30
+ Copyright (c) 2026-Present Datalayer, Inc.
31
+
32
+ Datalayer License
33
+ -->
34
+
35
+ [![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.ai)
36
+
37
+ # Reactor MCP Server
38
+
39
+ An extensible [MCP](https://modelcontextprotocol.io) server built on
40
+ [Reactor](https://github.com/datalayer/reactor). Three ideas, and the rest
41
+ follows from them.
42
+
43
+ ## A tool is a contribution
44
+
45
+ An extension does not reach into a server and register a function on it. It
46
+ *offers* a tool at a contribution point, and the host builds a server from what
47
+ has been offered:
48
+
49
+ ```python
50
+ from reactor import PluginManifest
51
+ from reactor_mcp_server import McpExtension, tool
52
+
53
+ class Sandboxes(McpExtension):
54
+ def manifest(self) -> PluginManifest:
55
+ return PluginManifest(name="sandboxes", version="1.0.0")
56
+
57
+ @tool(title="Launch Sandbox")
58
+ async def launch_sandbox(self, sandbox_name: str, variant: str = "eval") -> dict:
59
+ """Launch a sandbox for this session to run code in."""
60
+ return await launch(sandbox_name, variant)
61
+ ```
62
+
63
+ Offering rather than registering is what makes the next part possible — and it
64
+ means what a server is made of can be read, listed and drawn before one is
65
+ built.
66
+
67
+ ## A contribution can be extended
68
+
69
+ An extension may narrow, wrap or re-describe a tool **another** extension
70
+ offered:
71
+
72
+ ```python
73
+ class DatalayerSandboxes(McpExtension):
74
+ def manifest(self) -> PluginManifest:
75
+ return PluginManifest(name="sandboxes-datalayer", version="1.0.0")
76
+
77
+ def tool_extensions(self):
78
+ def on_datalayer(handler):
79
+ async def narrowed(sandbox_name: str, environment: str = "ai-env") -> dict:
80
+ return await handler(sandbox_name, variant="datalayer")
81
+ return narrowed
82
+
83
+ return (("launch_sandbox", ToolExtension(
84
+ wrap=on_datalayer,
85
+ description="Launch a sandbox on Datalayer.",
86
+ )),)
87
+ ```
88
+
89
+ Without this, an extension that wants to narrow somebody else's tool has one
90
+ lever: register a tool of the same name and hope to load second, because the
91
+ SDK keeps the first registration. That makes the outcome depend on the order
92
+ two distributions happened to be installed in — it works where the entry point
93
+ names sort the right way and silently does nothing where they do not.
94
+ Extensions are applied by name, in a declared order, on every machine.
95
+
96
+ ## The URL decides what is served
97
+
98
+ Tools belong to named toolsets, and a client says which ones it wants in the
99
+ URL it connects to:
100
+
101
+ | URL | What the client gets |
102
+ | --- | --- |
103
+ | `https://mcp.example.com/mcp` | every toolset that is on by default |
104
+ | `https://mcp.example.com/mcp?benchmarks` | the defaults, and `benchmarks` |
105
+ | `https://mcp.example.com/mcp?toolsets=spaces,library` | the defaults, and those two |
106
+ | `https://mcp.example.com/mcp?only=spaces` | `spaces` alone, plus the always-on toolsets |
107
+ | `https://mcp.example.com/mcp?without=sandboxes` | the defaults, less that one |
108
+
109
+ A toolset that is `default=False` costs a client that did not ask for it
110
+ nothing: the extension offering it declares
111
+ `activation_events=[on_toolset("benchmarks")]` and is not registered at all
112
+ until a URL names it.
113
+
114
+ `GET /toolsets` answers what a deployment offers and what a given URL would
115
+ activate, so a client can be told which name to put in its URL.
116
+
117
+ ## Running it
118
+
119
+ ```bash
120
+ pip install reactor_mcp_server
121
+ reactor-mcp-server --port 4040
122
+ ```
123
+
124
+ Every extension installed on the `reactor.mcp.extensions` entry-point group is
125
+ discovered, started as a reactor plugin, and served. A deployment adds a
126
+ toolset by installing a distribution:
127
+
128
+ ```toml
129
+ [project.entry-points."reactor.mcp.extensions"]
130
+ sandboxes = "my_package:SandboxesExtension"
131
+ ```
132
+
133
+ `REACTOR_MCP_EXTENSIONS=a,b` narrows discovery to the names it lists, which is
134
+ what makes a tool list reproducible on a machine that happens to carry an extra
135
+ extension.
136
+
137
+ ## In a server of your own
138
+
139
+ ```python
140
+ from reactor_mcp_server import build_host, create_mcp_app, load_extensions
141
+
142
+ host = build_host(load_extensions())
143
+ app = create_mcp_app(host, path="/mcp")
144
+ ```
145
+
146
+ `McpHost` owns the reactor platform, so activation events, enablement,
147
+ per-tenant scoping and disposal work as they do for any other reactor plugin,
148
+ and a server is built once per distinct set of toolsets and shared.
149
+
150
+ ## License
151
+
152
+ Datalayer License.
@@ -0,0 +1,124 @@
1
+ <!--
2
+ Copyright (c) 2026-Present Datalayer, Inc.
3
+
4
+ Datalayer License
5
+ -->
6
+
7
+ [![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.ai)
8
+
9
+ # Reactor MCP Server
10
+
11
+ An extensible [MCP](https://modelcontextprotocol.io) server built on
12
+ [Reactor](https://github.com/datalayer/reactor). Three ideas, and the rest
13
+ follows from them.
14
+
15
+ ## A tool is a contribution
16
+
17
+ An extension does not reach into a server and register a function on it. It
18
+ *offers* a tool at a contribution point, and the host builds a server from what
19
+ has been offered:
20
+
21
+ ```python
22
+ from reactor import PluginManifest
23
+ from reactor_mcp_server import McpExtension, tool
24
+
25
+ class Sandboxes(McpExtension):
26
+ def manifest(self) -> PluginManifest:
27
+ return PluginManifest(name="sandboxes", version="1.0.0")
28
+
29
+ @tool(title="Launch Sandbox")
30
+ async def launch_sandbox(self, sandbox_name: str, variant: str = "eval") -> dict:
31
+ """Launch a sandbox for this session to run code in."""
32
+ return await launch(sandbox_name, variant)
33
+ ```
34
+
35
+ Offering rather than registering is what makes the next part possible — and it
36
+ means what a server is made of can be read, listed and drawn before one is
37
+ built.
38
+
39
+ ## A contribution can be extended
40
+
41
+ An extension may narrow, wrap or re-describe a tool **another** extension
42
+ offered:
43
+
44
+ ```python
45
+ class DatalayerSandboxes(McpExtension):
46
+ def manifest(self) -> PluginManifest:
47
+ return PluginManifest(name="sandboxes-datalayer", version="1.0.0")
48
+
49
+ def tool_extensions(self):
50
+ def on_datalayer(handler):
51
+ async def narrowed(sandbox_name: str, environment: str = "ai-env") -> dict:
52
+ return await handler(sandbox_name, variant="datalayer")
53
+ return narrowed
54
+
55
+ return (("launch_sandbox", ToolExtension(
56
+ wrap=on_datalayer,
57
+ description="Launch a sandbox on Datalayer.",
58
+ )),)
59
+ ```
60
+
61
+ Without this, an extension that wants to narrow somebody else's tool has one
62
+ lever: register a tool of the same name and hope to load second, because the
63
+ SDK keeps the first registration. That makes the outcome depend on the order
64
+ two distributions happened to be installed in — it works where the entry point
65
+ names sort the right way and silently does nothing where they do not.
66
+ Extensions are applied by name, in a declared order, on every machine.
67
+
68
+ ## The URL decides what is served
69
+
70
+ Tools belong to named toolsets, and a client says which ones it wants in the
71
+ URL it connects to:
72
+
73
+ | URL | What the client gets |
74
+ | --- | --- |
75
+ | `https://mcp.example.com/mcp` | every toolset that is on by default |
76
+ | `https://mcp.example.com/mcp?benchmarks` | the defaults, and `benchmarks` |
77
+ | `https://mcp.example.com/mcp?toolsets=spaces,library` | the defaults, and those two |
78
+ | `https://mcp.example.com/mcp?only=spaces` | `spaces` alone, plus the always-on toolsets |
79
+ | `https://mcp.example.com/mcp?without=sandboxes` | the defaults, less that one |
80
+
81
+ A toolset that is `default=False` costs a client that did not ask for it
82
+ nothing: the extension offering it declares
83
+ `activation_events=[on_toolset("benchmarks")]` and is not registered at all
84
+ until a URL names it.
85
+
86
+ `GET /toolsets` answers what a deployment offers and what a given URL would
87
+ activate, so a client can be told which name to put in its URL.
88
+
89
+ ## Running it
90
+
91
+ ```bash
92
+ pip install reactor_mcp_server
93
+ reactor-mcp-server --port 4040
94
+ ```
95
+
96
+ Every extension installed on the `reactor.mcp.extensions` entry-point group is
97
+ discovered, started as a reactor plugin, and served. A deployment adds a
98
+ toolset by installing a distribution:
99
+
100
+ ```toml
101
+ [project.entry-points."reactor.mcp.extensions"]
102
+ sandboxes = "my_package:SandboxesExtension"
103
+ ```
104
+
105
+ `REACTOR_MCP_EXTENSIONS=a,b` narrows discovery to the names it lists, which is
106
+ what makes a tool list reproducible on a machine that happens to carry an extra
107
+ extension.
108
+
109
+ ## In a server of your own
110
+
111
+ ```python
112
+ from reactor_mcp_server import build_host, create_mcp_app, load_extensions
113
+
114
+ host = build_host(load_extensions())
115
+ app = create_mcp_app(host, path="/mcp")
116
+ ```
117
+
118
+ `McpHost` owns the reactor platform, so activation events, enablement,
119
+ per-tenant scoping and disposal work as they do for any other reactor plugin,
120
+ and a server is built once per distinct set of toolsets and shared.
121
+
122
+ ## License
123
+
124
+ Datalayer License.
@@ -0,0 +1,51 @@
1
+ # Copyright (c) 2026-Present Datalayer, Inc.
2
+ #
3
+ # Datalayer License
4
+
5
+ [build-system]
6
+ requires = ["hatchling"]
7
+ build-backend = "hatchling.build"
8
+
9
+ [project]
10
+ name = "reactor_mcp_server"
11
+ version = "1.0.1"
12
+ description = "An extensible MCP server built on Reactor: tools are contributions, and a contribution can be extended"
13
+ readme = "README.md"
14
+ license = { text = "Datalayer License" }
15
+ requires-python = ">=3.10"
16
+ authors = [{ name = "Datalayer", email = "support@datalayer.io" }]
17
+ keywords = ["mcp", "reactor", "plugins", "tools"]
18
+ classifiers = [
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ ]
26
+ dependencies = [
27
+ "datalayer_reactor>=1.0.2",
28
+ "mcp>=2,<3",
29
+ "starlette",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ server = ["uvicorn[standard]"]
34
+ test = ["pytest", "pytest-asyncio", "httpx", "uvicorn[standard]"]
35
+
36
+ [project.scripts]
37
+ reactor-mcp-server = "reactor_mcp_server.__main__:main"
38
+
39
+ # An MCP extension is discovered here. One group, so a distribution that
40
+ # carries several extensions lists them all and a host loads them by name.
41
+ [project.entry-points."reactor.mcp.extensions"]
42
+
43
+ [project.urls]
44
+ Homepage = "https://datalayer.ai"
45
+ Source = "https://github.com/datalayer/reactor"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["reactor_mcp_server"]
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
@@ -0,0 +1,93 @@
1
+ # Copyright (c) 2026-Present Datalayer, Inc.
2
+ #
3
+ # Datalayer License
4
+
5
+ """An extensible MCP server built on Reactor.
6
+
7
+ Three ideas, and the rest follows:
8
+
9
+ * **A tool is a contribution.** An extension offers tools at a contribution
10
+ point instead of registering them on a server object, so what a server is
11
+ made of can be read before one is built.
12
+ * **A contribution can be extended.** An extension may narrow, wrap or
13
+ re-describe a tool another extension offered, by name and in a declared
14
+ order — rather than registering a tool of the same name and hoping to load
15
+ second.
16
+ * **The URL decides what is served.** Tools belong to named toolsets, and a
17
+ client says which ones it wants in the URL it connects to:
18
+ ``https://mcp.example.com/mcp?benchmarks``.
19
+
20
+ @module reactor_mcp_server
21
+ """
22
+
23
+ from reactor_mcp_server.app import ToolsetRouter, create_mcp_app
24
+ from reactor_mcp_server.extension import (
25
+ McpExtension,
26
+ load_extensions,
27
+ manifest_of,
28
+ )
29
+ from reactor_mcp_server.points import (
30
+ ENTRY_POINT_GROUP,
31
+ PROMPTS,
32
+ RESOURCES,
33
+ TOOLS,
34
+ TOOLSETS,
35
+ on_toolset,
36
+ )
37
+ from reactor_mcp_server.server import (
38
+ BuiltServer,
39
+ McpHost,
40
+ build_host,
41
+ put_on_server,
42
+ )
43
+ from reactor_mcp_server.tools import (
44
+ Handler,
45
+ ToolExtension,
46
+ ToolSpec,
47
+ Wrapper,
48
+ resolve,
49
+ tool,
50
+ tool_spec_of,
51
+ )
52
+ from reactor_mcp_server.toolsets import (
53
+ Selection,
54
+ Toolset,
55
+ activation_key,
56
+ active_toolsets,
57
+ parse_selection,
58
+ unknown_names,
59
+ )
60
+
61
+ __version__ = "1.0.1"
62
+
63
+ __all__ = [
64
+ "BuiltServer",
65
+ "ENTRY_POINT_GROUP",
66
+ "Handler",
67
+ "McpExtension",
68
+ "McpHost",
69
+ "PROMPTS",
70
+ "RESOURCES",
71
+ "Selection",
72
+ "TOOLS",
73
+ "TOOLSETS",
74
+ "ToolExtension",
75
+ "ToolSpec",
76
+ "Toolset",
77
+ "ToolsetRouter",
78
+ "Wrapper",
79
+ "__version__",
80
+ "activation_key",
81
+ "active_toolsets",
82
+ "build_host",
83
+ "create_mcp_app",
84
+ "load_extensions",
85
+ "manifest_of",
86
+ "on_toolset",
87
+ "parse_selection",
88
+ "put_on_server",
89
+ "resolve",
90
+ "tool",
91
+ "tool_spec_of",
92
+ "unknown_names",
93
+ ]
@@ -0,0 +1,71 @@
1
+ # Copyright (c) 2026-Present Datalayer, Inc.
2
+ #
3
+ # Datalayer License
4
+
5
+ """Run the installed MCP extensions as a reactor application.
6
+
7
+ ``python -m reactor_mcp_server`` — or ``reactor-mcp-server`` — discovers every
8
+ extension on the ``reactor.mcp.extensions`` entry-point group, starts a reactor
9
+ platform with them, and serves them over streamable HTTP. A deployment adds a
10
+ toolset by installing a distribution; nothing here is edited.
11
+
12
+ @module reactor_mcp_server.__main__
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import logging
19
+ import os
20
+
21
+ from reactor_mcp_server.app import create_mcp_app
22
+ from reactor_mcp_server.extension import load_extensions
23
+ from reactor_mcp_server.server import build_host
24
+
25
+ #: Entry-point names to load, to the exclusion of everything else installed.
26
+ #: What makes a tool list reproducible on a machine that happens to carry an
27
+ #: extra extension.
28
+ EXTENSIONS_ENV = "REACTOR_MCP_EXTENSIONS"
29
+
30
+
31
+ def parser() -> argparse.ArgumentParser:
32
+ parsed = argparse.ArgumentParser(
33
+ prog="reactor-mcp-server",
34
+ description="Serve the installed MCP extensions over streamable HTTP.",
35
+ )
36
+ parsed.add_argument("--host", default=os.environ.get("REACTOR_MCP_HOST", "0.0.0.0"))
37
+ parsed.add_argument(
38
+ "--port", type=int, default=int(os.environ.get("REACTOR_MCP_PORT", "4040"))
39
+ )
40
+ parsed.add_argument("--path", default=os.environ.get("REACTOR_MCP_PATH", "/mcp"))
41
+ parsed.add_argument(
42
+ "--name", default=os.environ.get("REACTOR_MCP_NAME", "reactor-mcp-server")
43
+ )
44
+ parsed.add_argument(
45
+ "--extension",
46
+ action="append",
47
+ default=None,
48
+ help="Load only this extension; repeatable.",
49
+ )
50
+ parsed.add_argument("--log-level", default=os.environ.get("REACTOR_MCP_LOG", "info"))
51
+ return parsed
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> None:
55
+ arguments = parser().parse_args(argv)
56
+ logging.basicConfig(level=arguments.log_level.upper())
57
+ names = arguments.extension or [
58
+ part.strip()
59
+ for part in (os.environ.get(EXTENSIONS_ENV) or "").split(",")
60
+ if part.strip()
61
+ ]
62
+ host = build_host(load_extensions(names or None), name=arguments.name)
63
+ app = create_mcp_app(host, path=arguments.path)
64
+
65
+ import uvicorn # noqa: PLC0415 - only the serving path needs it
66
+
67
+ uvicorn.run(app, host=arguments.host, port=arguments.port, log_level=arguments.log_level)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()