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.
- reactor_mcp_server-1.0.1/.gitignore +1 -0
- reactor_mcp_server-1.0.1/PKG-INFO +152 -0
- reactor_mcp_server-1.0.1/README.md +124 -0
- reactor_mcp_server-1.0.1/pyproject.toml +51 -0
- reactor_mcp_server-1.0.1/reactor_mcp_server/__init__.py +93 -0
- reactor_mcp_server-1.0.1/reactor_mcp_server/__main__.py +71 -0
- reactor_mcp_server-1.0.1/reactor_mcp_server/app.py +244 -0
- reactor_mcp_server-1.0.1/reactor_mcp_server/extension.py +204 -0
- reactor_mcp_server-1.0.1/reactor_mcp_server/points.py +53 -0
- reactor_mcp_server-1.0.1/reactor_mcp_server/server.py +262 -0
- reactor_mcp_server-1.0.1/reactor_mcp_server/tools.py +182 -0
- reactor_mcp_server-1.0.1/reactor_mcp_server/toolsets.py +154 -0
- reactor_mcp_server-1.0.1/tests/test_activation_by_url.py +203 -0
- reactor_mcp_server-1.0.1/tests/test_served_over_http.py +287 -0
- reactor_mcp_server-1.0.1/tests/test_tools_are_contributions.py +334 -0
|
@@ -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
|
+
[](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
|
+
[](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()
|