iflow-mcp_modelcontextinterface-mcix 1.1.1.dev0__py3-none-any.whl
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.
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/METADATA +931 -0
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/RECORD +42 -0
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/WHEEL +4 -0
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/entry_points.txt +2 -0
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/licenses/LICENSE +21 -0
- mci/__init__.py +10 -0
- mci/assets/example_toolset.mci.json +37 -0
- mci/assets/example_toolset.mci.yaml +23 -0
- mci/assets/gitignore +1 -0
- mci/assets/mci.json +29 -0
- mci/assets/mci.yaml +19 -0
- mci/cli/__init__.py +8 -0
- mci/cli/add.py +108 -0
- mci/cli/envs.py +257 -0
- mci/cli/formatters/__init__.py +12 -0
- mci/cli/formatters/env_formatter.py +83 -0
- mci/cli/formatters/json_formatter.py +93 -0
- mci/cli/formatters/table_formatter.py +138 -0
- mci/cli/formatters/yaml_formatter.py +93 -0
- mci/cli/install.py +147 -0
- mci/cli/list.py +153 -0
- mci/cli/run.py +125 -0
- mci/cli/validate.py +113 -0
- mci/core/__init__.py +8 -0
- mci/core/config.py +144 -0
- mci/core/dynamic_server.py +187 -0
- mci/core/file_finder.py +105 -0
- mci/core/mci_client.py +196 -0
- mci/core/mcp_server.py +240 -0
- mci/core/schema_editor.py +284 -0
- mci/core/tool_converter.py +119 -0
- mci/core/tool_manager.py +118 -0
- mci/core/validator.py +162 -0
- mci/mci.py +39 -0
- mci/py.typed +0 -0
- mci/utils/__init__.py +8 -0
- mci/utils/dotenv.py +170 -0
- mci/utils/env_scanner.py +84 -0
- mci/utils/error_formatter.py +165 -0
- mci/utils/error_handler.py +174 -0
- mci/utils/timestamp.py +50 -0
- mci/utils/validation.py +92 -0
mci/core/mci_client.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mci_client.py - CLI wrapper around MCIClient from mci-py
|
|
3
|
+
|
|
4
|
+
This module provides a CLI-friendly wrapper around the MCIClient class from mci-py.
|
|
5
|
+
It delegates all schema parsing, validation, tool loading, and filtering to MCIClient,
|
|
6
|
+
while providing CLI-specific error handling and output formatting. It also supports
|
|
7
|
+
automatic loading of environment variables from .env files.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from mcipy import MCIClient
|
|
13
|
+
from mcipy.models import Tool
|
|
14
|
+
|
|
15
|
+
from mci.utils.dotenv import get_env_with_dotenv
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MCIClientWrapper:
|
|
19
|
+
"""
|
|
20
|
+
CLI wrapper around MCIClient from mci-py.
|
|
21
|
+
|
|
22
|
+
This class provides a CLI-friendly interface to MCIClient, delegating all
|
|
23
|
+
tool loading, filtering, and schema operations to the upstream mci-py library.
|
|
24
|
+
It focuses on error handling and formatting for CLI usability.
|
|
25
|
+
|
|
26
|
+
The wrapper automatically loads environment variables from .env files in:
|
|
27
|
+
- The project root directory (same location as the MCI schema file)
|
|
28
|
+
- The ./mci directory
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self, file_path: str, env_vars: dict[str, str] | None = None, auto_load_dotenv: bool = True
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the wrapper with an MCIClient instance.
|
|
36
|
+
|
|
37
|
+
If auto_load_dotenv is True (default), automatically loads environment variables
|
|
38
|
+
from .env and .env.mci files. Priority order:
|
|
39
|
+
- If .env.mci files exist:
|
|
40
|
+
1. ./mci/.env.mci (library MCI-specific)
|
|
41
|
+
2. Project root .env.mci (project MCI-specific)
|
|
42
|
+
- If no .env.mci files exist:
|
|
43
|
+
1. ./mci/.env (library defaults)
|
|
44
|
+
2. Project root .env (project-level)
|
|
45
|
+
- Then:
|
|
46
|
+
3. System environment variables
|
|
47
|
+
4. env_vars argument (highest priority)
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
file_path: Path to the MCI schema file (.json, .yaml, or .yml)
|
|
51
|
+
env_vars: Optional environment variables for template substitution (highest priority)
|
|
52
|
+
auto_load_dotenv: Whether to automatically load .env files (default: True)
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
MCIClientError: If the schema file cannot be loaded or parsed
|
|
56
|
+
"""
|
|
57
|
+
# Determine project root from schema file location
|
|
58
|
+
project_root = Path(file_path).parent.resolve()
|
|
59
|
+
|
|
60
|
+
# Load environment variables with proper precedence
|
|
61
|
+
if auto_load_dotenv:
|
|
62
|
+
merged_env = get_env_with_dotenv(project_root, env_vars)
|
|
63
|
+
else:
|
|
64
|
+
# If auto-loading is disabled, just use provided env_vars
|
|
65
|
+
merged_env = env_vars or {}
|
|
66
|
+
|
|
67
|
+
self._client: MCIClient = MCIClient(schema_file_path=file_path, env_vars=merged_env)
|
|
68
|
+
self._file_path: str = file_path
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def client(self) -> MCIClient:
|
|
72
|
+
"""Get the underlying MCIClient instance."""
|
|
73
|
+
return self._client
|
|
74
|
+
|
|
75
|
+
def get_tools(self) -> list[Tool]:
|
|
76
|
+
"""
|
|
77
|
+
Get all available tools from the loaded schema.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of Tool objects from the schema
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> wrapper = MCIClientWrapper("mci.json")
|
|
84
|
+
>>> tools = wrapper.get_tools()
|
|
85
|
+
>>> print([t.name for t in tools])
|
|
86
|
+
"""
|
|
87
|
+
return self._client.tools()
|
|
88
|
+
|
|
89
|
+
def filter_only(self, tool_names: list[str]) -> list[Tool]:
|
|
90
|
+
"""
|
|
91
|
+
Filter tools to include only specified tools by name.
|
|
92
|
+
|
|
93
|
+
This method delegates to MCIClient.only() to perform filtering.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
tool_names: List of tool names to include
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Filtered list of Tool objects matching the specified names
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> wrapper = MCIClientWrapper("mci.json")
|
|
103
|
+
>>> tools = wrapper.filter_only(["get_weather", "get_forecast"])
|
|
104
|
+
>>> print([t.name for t in tools])
|
|
105
|
+
"""
|
|
106
|
+
return self._client.only(tool_names)
|
|
107
|
+
|
|
108
|
+
def filter_except(self, tool_names: list[str]) -> list[Tool]:
|
|
109
|
+
"""
|
|
110
|
+
Filter tools to exclude specified tools by name.
|
|
111
|
+
|
|
112
|
+
This method delegates to MCIClient.without() to perform filtering.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
tool_names: List of tool names to exclude
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Filtered list of Tool objects excluding the specified names
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> wrapper = MCIClientWrapper("mci.json")
|
|
122
|
+
>>> tools = wrapper.filter_except(["delete_data", "admin_tools"])
|
|
123
|
+
>>> print([t.name for t in tools])
|
|
124
|
+
"""
|
|
125
|
+
return self._client.without(tool_names)
|
|
126
|
+
|
|
127
|
+
def filter_tags(self, tags: list[str]) -> list[Tool]:
|
|
128
|
+
"""
|
|
129
|
+
Filter tools to include only those with at least one matching tag.
|
|
130
|
+
|
|
131
|
+
This method delegates to MCIClient.tags() to perform filtering.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
tags: List of tags to filter by (OR logic - tool must have at least one matching tag)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Filtered list of Tool objects that have at least one of the specified tags
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
>>> wrapper = MCIClientWrapper("mci.json")
|
|
141
|
+
>>> tools = wrapper.filter_tags(["api", "database"])
|
|
142
|
+
>>> print([t.name for t in tools])
|
|
143
|
+
"""
|
|
144
|
+
return self._client.tags(tags)
|
|
145
|
+
|
|
146
|
+
def filter_without_tags(self, tags: list[str]) -> list[Tool]:
|
|
147
|
+
"""
|
|
148
|
+
Filter tools to exclude those with any matching tag.
|
|
149
|
+
|
|
150
|
+
This method delegates to MCIClient.withoutTags() to perform filtering.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
tags: List of tags to exclude (OR logic - tool is excluded if it has any matching tag)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Filtered list of Tool objects that do NOT have any of the specified tags
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> wrapper = MCIClientWrapper("mci.json")
|
|
160
|
+
>>> tools = wrapper.filter_without_tags(["external", "deprecated"])
|
|
161
|
+
>>> print([t.name for t in tools])
|
|
162
|
+
"""
|
|
163
|
+
return self._client.withoutTags(tags)
|
|
164
|
+
|
|
165
|
+
def filter_toolsets(self, toolset_names: list[str]) -> list[Tool]:
|
|
166
|
+
"""
|
|
167
|
+
Filter tools to include only those from specified toolsets.
|
|
168
|
+
|
|
169
|
+
This method delegates to MCIClient.toolsets() to perform filtering.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
toolset_names: List of toolset names to include
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Filtered list of Tool objects from the specified toolsets
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
>>> wrapper = MCIClientWrapper("mci.json")
|
|
179
|
+
>>> tools = wrapper.filter_toolsets(["weather", "database"])
|
|
180
|
+
>>> print([t.name for t in tools])
|
|
181
|
+
"""
|
|
182
|
+
return self._client.toolsets(toolset_names)
|
|
183
|
+
|
|
184
|
+
def list_tool_names(self) -> list[str]:
|
|
185
|
+
"""
|
|
186
|
+
List available tool names as strings.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of tool names (strings)
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
>>> wrapper = MCIClientWrapper("mci.json")
|
|
193
|
+
>>> names = wrapper.list_tool_names()
|
|
194
|
+
>>> print(names)
|
|
195
|
+
"""
|
|
196
|
+
return self._client.list_tools()
|
mci/core/mcp_server.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mcp_server.py - MCP server creation and management
|
|
3
|
+
|
|
4
|
+
This module provides infrastructure for creating MCP servers that serve MCI tools.
|
|
5
|
+
The servers keep MCIClient and tool definitions in memory, register tools as
|
|
6
|
+
MCP-compatible tools, and delegate execution back to MCIClient.
|
|
7
|
+
|
|
8
|
+
Note: This module dynamically adds attributes to MCP Server instances for storing
|
|
9
|
+
MCI-specific data. These attributes are prefixed with _mci_ to avoid conflicts.
|
|
10
|
+
Type checkers will report these as unknown attributes, which is expected.
|
|
11
|
+
"""
|
|
12
|
+
# pyright: reportAttributeAccessIssue=false
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import mcp.server.stdio
|
|
18
|
+
import mcp.types as types
|
|
19
|
+
from mcipy import MCIClient
|
|
20
|
+
from mcipy.models import Tool
|
|
21
|
+
from mcp.server.lowlevel import NotificationOptions, Server
|
|
22
|
+
from mcp.server.models import InitializationOptions
|
|
23
|
+
|
|
24
|
+
from mci.core.tool_converter import MCIToolConverter
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MCPServerBuilder:
|
|
28
|
+
"""
|
|
29
|
+
Builder for creating and configuring MCP servers with MCI tools.
|
|
30
|
+
|
|
31
|
+
This class provides methods to create MCP server instances and register
|
|
32
|
+
MCI tools as MCP-compatible tools. It maintains the MCIClient instance
|
|
33
|
+
in memory and delegates all tool execution to MCIClient.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, mci_client: MCIClient):
|
|
37
|
+
"""
|
|
38
|
+
Initialize the server builder with an MCIClient instance.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
mci_client: Initialized MCIClient instance with loaded tools
|
|
42
|
+
"""
|
|
43
|
+
self.mci_client: MCIClient = mci_client
|
|
44
|
+
self.converter: MCIToolConverter = MCIToolConverter()
|
|
45
|
+
|
|
46
|
+
async def create_server(self, name: str, version: str = "1.0.0") -> Server:
|
|
47
|
+
"""
|
|
48
|
+
Create an MCP server instance configured to serve MCI tools.
|
|
49
|
+
|
|
50
|
+
Creates a low-level MCP server that will expose MCI tools via the
|
|
51
|
+
MCP protocol. The server maintains the MCIClient instance in memory
|
|
52
|
+
and delegates tool execution to it.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
name: Server name for MCP protocol
|
|
56
|
+
version: Server version string
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Configured MCP Server instance
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> builder = MCPServerBuilder(mci_client)
|
|
63
|
+
>>> server = await builder.create_server("my-mci-server", "1.0.0")
|
|
64
|
+
"""
|
|
65
|
+
server = Server(name)
|
|
66
|
+
|
|
67
|
+
# Store reference to MCIClient for tool execution
|
|
68
|
+
server._mci_client = self.mci_client # type: ignore[attr-defined]
|
|
69
|
+
server._mci_converter = self.converter # type: ignore[attr-defined]
|
|
70
|
+
server._server_version = version # type: ignore[attr-defined]
|
|
71
|
+
|
|
72
|
+
return server
|
|
73
|
+
|
|
74
|
+
async def register_tool(self, server: Server, mci_tool: Tool) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Register a single MCI tool with the MCP server.
|
|
77
|
+
|
|
78
|
+
Converts the MCI tool to MCP format and adds it to the server's
|
|
79
|
+
tool registry. This does not define the handler yet - the handler
|
|
80
|
+
is defined separately when setting up the server.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
server: MCP Server instance
|
|
84
|
+
mci_tool: MCI Tool object to register
|
|
85
|
+
"""
|
|
86
|
+
# Convert MCI tool to MCP format
|
|
87
|
+
mcp_tool = self.converter.convert_to_mcp_tool(mci_tool)
|
|
88
|
+
|
|
89
|
+
# Store the tool in the server's internal registry
|
|
90
|
+
if not hasattr(server, "_mci_tools"):
|
|
91
|
+
server._mci_tools = [] # type: ignore[attr-defined]
|
|
92
|
+
|
|
93
|
+
server._mci_tools.append(mcp_tool) # type: ignore[attr-defined]
|
|
94
|
+
|
|
95
|
+
async def register_all_tools(self, server: Server, tools: list[Tool]) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Register multiple MCI tools with the MCP server.
|
|
98
|
+
|
|
99
|
+
Batch registration of tools. Each tool is converted to MCP format
|
|
100
|
+
and added to the server's tool registry.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
server: MCP Server instance
|
|
104
|
+
tools: List of MCI Tool objects to register
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> tools = mci_client.tools()
|
|
108
|
+
>>> await builder.register_all_tools(server, tools)
|
|
109
|
+
"""
|
|
110
|
+
for tool in tools:
|
|
111
|
+
await self.register_tool(server, tool)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ServerInstance:
|
|
115
|
+
"""
|
|
116
|
+
Runtime instance of an MCP server serving MCI tools.
|
|
117
|
+
|
|
118
|
+
This class manages the lifecycle of an MCP server, including startup,
|
|
119
|
+
shutdown, and tool execution handling. It delegates all tool execution
|
|
120
|
+
to the MCIClient instance.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self, server: Server, mci_client: MCIClient, env_vars: dict[str, str] | None = None
|
|
125
|
+
):
|
|
126
|
+
"""
|
|
127
|
+
Initialize the server instance.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
server: Configured MCP Server
|
|
131
|
+
mci_client: MCIClient instance for tool execution
|
|
132
|
+
env_vars: Optional environment variables for template substitution during execution
|
|
133
|
+
"""
|
|
134
|
+
self.server: Server = server
|
|
135
|
+
self.mci_client: MCIClient = mci_client
|
|
136
|
+
self.env_vars: dict[str, str] = env_vars or {}
|
|
137
|
+
self.converter: MCIToolConverter = MCIToolConverter()
|
|
138
|
+
self._running: bool = False
|
|
139
|
+
|
|
140
|
+
# Set up handlers
|
|
141
|
+
self._setup_handlers()
|
|
142
|
+
|
|
143
|
+
def _setup_handlers(self) -> None:
|
|
144
|
+
"""Set up MCP protocol handlers for tool listing and execution."""
|
|
145
|
+
|
|
146
|
+
@self.server.list_tools()
|
|
147
|
+
async def handle_list_tools() -> list[types.Tool]: # pyright: ignore[reportUnusedFunction]
|
|
148
|
+
"""List all available MCI tools in MCP format."""
|
|
149
|
+
if hasattr(self.server, "_mci_tools"):
|
|
150
|
+
return self.server._mci_tools # type: ignore[attr-defined]
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
@self.server.call_tool()
|
|
154
|
+
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]: # pyright: ignore[reportUnusedFunction]
|
|
155
|
+
"""
|
|
156
|
+
Handle tool execution by delegating to MCIClient.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
name: Tool name
|
|
160
|
+
arguments: Tool input arguments
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of TextContent with execution results
|
|
164
|
+
"""
|
|
165
|
+
return await self.handle_tool_call(name, arguments)
|
|
166
|
+
|
|
167
|
+
async def handle_tool_call(
|
|
168
|
+
self, name: str, arguments: dict[str, Any]
|
|
169
|
+
) -> list[types.TextContent]:
|
|
170
|
+
"""
|
|
171
|
+
Execute a tool using MCIClient and return MCP-formatted results.
|
|
172
|
+
|
|
173
|
+
This method delegates execution to MCIClient.execute() and converts
|
|
174
|
+
the result to MCP TextContent format.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
name: Name of the tool to execute
|
|
178
|
+
arguments: Input arguments for the tool
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of TextContent blocks with execution results
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ValueError: If tool is not found or execution fails
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
# Execute the tool using MCIClient
|
|
188
|
+
result = await asyncio.to_thread(
|
|
189
|
+
self.mci_client.execute, tool_name=name, properties=arguments
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Convert result to MCP TextContent format
|
|
193
|
+
# MCIClient.execute returns a dict with the execution result
|
|
194
|
+
result_text = str(result)
|
|
195
|
+
|
|
196
|
+
return [types.TextContent(type="text", text=result_text)]
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
# Return error as TextContent
|
|
200
|
+
error_msg = f"Tool execution failed: {str(e)}"
|
|
201
|
+
return [types.TextContent(type="text", text=error_msg)]
|
|
202
|
+
|
|
203
|
+
async def start(self, stdio: bool = True) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Start the MCP server.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
stdio: Whether to use STDIO transport (default: True)
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
RuntimeError: If server is already running
|
|
212
|
+
"""
|
|
213
|
+
if self._running:
|
|
214
|
+
raise RuntimeError("Server is already running")
|
|
215
|
+
|
|
216
|
+
self._running = True
|
|
217
|
+
|
|
218
|
+
if stdio:
|
|
219
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
220
|
+
await self.server.run(
|
|
221
|
+
read_stream,
|
|
222
|
+
write_stream,
|
|
223
|
+
InitializationOptions(
|
|
224
|
+
server_name=self.server.name,
|
|
225
|
+
server_version=getattr(self.server, "_server_version", "1.0.0"),
|
|
226
|
+
capabilities=self.server.get_capabilities(
|
|
227
|
+
notification_options=NotificationOptions(),
|
|
228
|
+
experimental_capabilities={},
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def stop(self) -> None:
|
|
234
|
+
"""
|
|
235
|
+
Stop the MCP server.
|
|
236
|
+
|
|
237
|
+
Marks the server as not running. The actual shutdown is handled
|
|
238
|
+
by the asyncio context manager in start().
|
|
239
|
+
"""
|
|
240
|
+
self._running = False
|