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.
Files changed (42) hide show
  1. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/METADATA +931 -0
  2. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/RECORD +42 -0
  3. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/WHEEL +4 -0
  4. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/licenses/LICENSE +21 -0
  6. mci/__init__.py +10 -0
  7. mci/assets/example_toolset.mci.json +37 -0
  8. mci/assets/example_toolset.mci.yaml +23 -0
  9. mci/assets/gitignore +1 -0
  10. mci/assets/mci.json +29 -0
  11. mci/assets/mci.yaml +19 -0
  12. mci/cli/__init__.py +8 -0
  13. mci/cli/add.py +108 -0
  14. mci/cli/envs.py +257 -0
  15. mci/cli/formatters/__init__.py +12 -0
  16. mci/cli/formatters/env_formatter.py +83 -0
  17. mci/cli/formatters/json_formatter.py +93 -0
  18. mci/cli/formatters/table_formatter.py +138 -0
  19. mci/cli/formatters/yaml_formatter.py +93 -0
  20. mci/cli/install.py +147 -0
  21. mci/cli/list.py +153 -0
  22. mci/cli/run.py +125 -0
  23. mci/cli/validate.py +113 -0
  24. mci/core/__init__.py +8 -0
  25. mci/core/config.py +144 -0
  26. mci/core/dynamic_server.py +187 -0
  27. mci/core/file_finder.py +105 -0
  28. mci/core/mci_client.py +196 -0
  29. mci/core/mcp_server.py +240 -0
  30. mci/core/schema_editor.py +284 -0
  31. mci/core/tool_converter.py +119 -0
  32. mci/core/tool_manager.py +118 -0
  33. mci/core/validator.py +162 -0
  34. mci/mci.py +39 -0
  35. mci/py.typed +0 -0
  36. mci/utils/__init__.py +8 -0
  37. mci/utils/dotenv.py +170 -0
  38. mci/utils/env_scanner.py +84 -0
  39. mci/utils/error_formatter.py +165 -0
  40. mci/utils/error_handler.py +174 -0
  41. mci/utils/timestamp.py +50 -0
  42. 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