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/cli/run.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
run.py - Run command for starting MCP servers from MCI schemas
|
|
3
|
+
|
|
4
|
+
This module implements the `mcix run` command, which launches an MCP server
|
|
5
|
+
over STDIO that dynamically serves tools from an MCI schema file. The server
|
|
6
|
+
loads tools using MCIClient, converts them to MCP format, and delegates
|
|
7
|
+
execution back to MCIClient.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from mcipy import MCIClientError
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from mci.core.dynamic_server import run_server
|
|
18
|
+
from mci.core.file_finder import MCIFileFinder
|
|
19
|
+
from mci.utils.error_handler import ErrorHandler
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command()
|
|
23
|
+
@click.option(
|
|
24
|
+
"--file",
|
|
25
|
+
"-f",
|
|
26
|
+
type=click.Path(exists=True),
|
|
27
|
+
default=None,
|
|
28
|
+
help="Path to MCI schema file (defaults to mci.json or mci.yaml in current directory)",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--filter",
|
|
32
|
+
type=str,
|
|
33
|
+
default=None,
|
|
34
|
+
help="Filter tools (format: type:value1,value2 - e.g., tags:api,database)",
|
|
35
|
+
)
|
|
36
|
+
def run(file: str | None, filter: str | None):
|
|
37
|
+
"""
|
|
38
|
+
Run an MCP server that serves tools from an MCI schema.
|
|
39
|
+
|
|
40
|
+
Launches an MCP server over STDIO that dynamically loads and serves tools
|
|
41
|
+
from the specified MCI schema file. The server uses MCIClient to load tools
|
|
42
|
+
(including toolsets and environment variable templating), converts them to
|
|
43
|
+
MCP format, and delegates execution back to MCIClient.
|
|
44
|
+
|
|
45
|
+
The server supports:
|
|
46
|
+
- Loading tools from JSON/YAML MCI schemas
|
|
47
|
+
- Tool filtering by name, tags, and toolsets
|
|
48
|
+
- Environment variable templating
|
|
49
|
+
- Graceful shutdown on Ctrl+C
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
|
|
53
|
+
# Run server with default mci.json or mci.yaml
|
|
54
|
+
mcix run
|
|
55
|
+
|
|
56
|
+
# Run server with specific file
|
|
57
|
+
mcix run --file=./custom.mci.json
|
|
58
|
+
|
|
59
|
+
# Run server with filtered tools
|
|
60
|
+
mcix run --filter=tags:api,database
|
|
61
|
+
|
|
62
|
+
# Run server with only specific tools
|
|
63
|
+
mcix run --filter=only:tool1,tool2
|
|
64
|
+
|
|
65
|
+
# Run server excluding specific tools
|
|
66
|
+
mcix run --filter=except:tool3,tool4
|
|
67
|
+
|
|
68
|
+
# Run server with tools from specific toolsets
|
|
69
|
+
mcix run --filter=toolsets:weather,database
|
|
70
|
+
"""
|
|
71
|
+
console = Console()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# Step 1: Find MCI file
|
|
75
|
+
if file is None:
|
|
76
|
+
finder = MCIFileFinder()
|
|
77
|
+
file = finder.find_mci_file()
|
|
78
|
+
if file is None:
|
|
79
|
+
console.print(
|
|
80
|
+
"[red]✗[/red] No MCI schema file found. "
|
|
81
|
+
"Run 'mcix install' to create one or specify --file.",
|
|
82
|
+
style="red",
|
|
83
|
+
)
|
|
84
|
+
raise click.Abort()
|
|
85
|
+
|
|
86
|
+
# Step 2: Validate filter spec if provided
|
|
87
|
+
if filter:
|
|
88
|
+
# Validate filter format early to provide better error messages
|
|
89
|
+
from mci.core.tool_manager import ToolManager
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
ToolManager.parse_filter_spec(filter)
|
|
93
|
+
except ValueError as e:
|
|
94
|
+
console.print(f"[red]✗[/red] Invalid filter: {e}", style="red")
|
|
95
|
+
raise click.Abort() from e
|
|
96
|
+
|
|
97
|
+
# Step 3: Gather environment variables for templating
|
|
98
|
+
env_vars = dict(os.environ)
|
|
99
|
+
|
|
100
|
+
# Step 4: Display startup message
|
|
101
|
+
console.print("[green]⚡[/green] Starting MCP server...", style="bold green")
|
|
102
|
+
console.print(f"[cyan]📄 Schema:[/cyan] {file}")
|
|
103
|
+
if filter:
|
|
104
|
+
console.print(f"[cyan]🔍 Filter:[/cyan] {filter}")
|
|
105
|
+
console.print()
|
|
106
|
+
console.print("[dim]Press Ctrl+C to stop the server[/dim]")
|
|
107
|
+
console.print()
|
|
108
|
+
|
|
109
|
+
# Step 5: Run the server (this blocks until Ctrl+C)
|
|
110
|
+
asyncio.run(run_server(file, filter, env_vars))
|
|
111
|
+
|
|
112
|
+
except KeyboardInterrupt:
|
|
113
|
+
# Graceful shutdown on Ctrl+C
|
|
114
|
+
console.print()
|
|
115
|
+
console.print("[yellow]⏹[/yellow] Server stopped by user", style="yellow")
|
|
116
|
+
except click.Abort:
|
|
117
|
+
raise
|
|
118
|
+
except MCIClientError as e:
|
|
119
|
+
console.print()
|
|
120
|
+
console.print(ErrorHandler.format_mci_client_error(e))
|
|
121
|
+
raise click.Abort() from e
|
|
122
|
+
except Exception as e:
|
|
123
|
+
console.print()
|
|
124
|
+
console.print(ErrorHandler.format_generic_error(e))
|
|
125
|
+
raise click.Abort() from e
|
mci/cli/validate.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
validate.py - CLI command to validate MCI schemas
|
|
3
|
+
|
|
4
|
+
This module provides the `mcix validate` command which checks MCI schema
|
|
5
|
+
correctness using mci-py's built-in validation and provides user-friendly
|
|
6
|
+
error and warning messages.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from mci.core.file_finder import MCIFileFinder
|
|
15
|
+
from mci.core.validator import MCIValidator
|
|
16
|
+
from mci.utils.error_formatter import ErrorFormatter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command()
|
|
20
|
+
@click.option(
|
|
21
|
+
"--file",
|
|
22
|
+
"-f",
|
|
23
|
+
default=None,
|
|
24
|
+
help="Path to MCI schema file (default: auto-discover mci.json/mci.yaml)",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--env",
|
|
28
|
+
"-e",
|
|
29
|
+
multiple=True,
|
|
30
|
+
help="Environment variables in KEY=VALUE format (can be specified multiple times)",
|
|
31
|
+
)
|
|
32
|
+
def validate(file: str | None, env: tuple[str, ...]) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Validate MCI schema file for correctness.
|
|
35
|
+
|
|
36
|
+
This command validates an MCI schema file using mci-py's built-in validation
|
|
37
|
+
engine. It checks for:
|
|
38
|
+
- Schema structure and syntax
|
|
39
|
+
- Required fields and data types
|
|
40
|
+
- Valid references and tool definitions
|
|
41
|
+
|
|
42
|
+
Additionally, it provides warnings for:
|
|
43
|
+
- Missing toolset files
|
|
44
|
+
- MCP commands not in PATH
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
mcix validate # Validate default mci.json/mci.yaml
|
|
48
|
+
mcix validate --file custom.mci.json # Validate specific file
|
|
49
|
+
mcix validate -e API_KEY=123 # Provide environment variables
|
|
50
|
+
"""
|
|
51
|
+
formatter = ErrorFormatter()
|
|
52
|
+
|
|
53
|
+
# Parse environment variables
|
|
54
|
+
env_vars = {}
|
|
55
|
+
for env_pair in env:
|
|
56
|
+
if "=" in env_pair:
|
|
57
|
+
key, value = env_pair.split("=", 1)
|
|
58
|
+
env_vars[key] = value
|
|
59
|
+
else:
|
|
60
|
+
formatter.console.print(
|
|
61
|
+
f"[yellow]Warning: Invalid environment variable format: {env_pair}. "
|
|
62
|
+
"Expected KEY=VALUE.[/yellow]"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Merge environment variables: user-provided env vars override system environment variables
|
|
66
|
+
merged_env = {**os.environ, **env_vars}
|
|
67
|
+
|
|
68
|
+
# Find the schema file
|
|
69
|
+
if file is None:
|
|
70
|
+
file_finder = MCIFileFinder()
|
|
71
|
+
file = file_finder.find_mci_file()
|
|
72
|
+
if file is None:
|
|
73
|
+
formatter.console.print("[red]❌ No MCI schema file found in current directory[/red]\n")
|
|
74
|
+
formatter.console.print(
|
|
75
|
+
"[yellow]💡 Run 'mcix install' to create a default mci.json file[/yellow]"
|
|
76
|
+
)
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
# Validate the schema
|
|
80
|
+
validator = MCIValidator(file_path=file, env_vars=merged_env)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
result = validator.validate_schema()
|
|
84
|
+
|
|
85
|
+
# Display errors
|
|
86
|
+
if result.errors:
|
|
87
|
+
formatter.format_validation_errors(result.errors)
|
|
88
|
+
formatter.console.print(
|
|
89
|
+
"\n[yellow]💡 Fix the errors above and run 'mcix validate' again[/yellow]\n"
|
|
90
|
+
)
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
# Display warnings (if any)
|
|
94
|
+
if result.warnings:
|
|
95
|
+
formatter.format_validation_warnings(result.warnings)
|
|
96
|
+
|
|
97
|
+
# Display success message
|
|
98
|
+
formatter.format_validation_success(file)
|
|
99
|
+
|
|
100
|
+
# Exit with appropriate code
|
|
101
|
+
sys.exit(0)
|
|
102
|
+
|
|
103
|
+
except FileNotFoundError as e:
|
|
104
|
+
formatter.format_mci_error(f"File not found: {str(e)}")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
formatter.format_mci_error(f"Unexpected error: {str(e)}")
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Allow running as script
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
validate()
|
mci/core/__init__.py
ADDED
mci/core/config.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
config.py - Configuration loading and validation for MCI files
|
|
3
|
+
|
|
4
|
+
This module provides functionality to load and validate MCI configuration files
|
|
5
|
+
using the MCIClient from mci-py. It handles schema validation, error handling,
|
|
6
|
+
and provides user-friendly error messages. It also automatically loads environment
|
|
7
|
+
variables from .env files in the project root and ./mci directory.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from mcipy import MCIClient, MCIClientError
|
|
13
|
+
|
|
14
|
+
from mci.utils.dotenv import get_env_with_dotenv
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MCIConfig:
|
|
18
|
+
"""
|
|
19
|
+
Manages MCI configuration file loading and validation.
|
|
20
|
+
|
|
21
|
+
This class provides methods to load MCI configuration files using the
|
|
22
|
+
MCIClient from mci-py, which performs built-in schema validation.
|
|
23
|
+
It also provides utilities for validating schemas and extracting
|
|
24
|
+
user-friendly error messages.
|
|
25
|
+
|
|
26
|
+
The class 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
|
+
Variables are merged with project root taking precedence over ./mci/.env.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def load(
|
|
35
|
+
file_path: str, env_vars: dict[str, str] | None = None, auto_load_dotenv: bool = True
|
|
36
|
+
) -> MCIClient:
|
|
37
|
+
"""
|
|
38
|
+
Load and parse an MCI configuration file using MCIClient.
|
|
39
|
+
|
|
40
|
+
This method uses MCIClient from mci-py to load and validate the schema.
|
|
41
|
+
The MCIClient performs comprehensive schema validation during initialization.
|
|
42
|
+
|
|
43
|
+
If auto_load_dotenv is True (default), automatically loads environment variables
|
|
44
|
+
from .env and .env.mci files. Priority order:
|
|
45
|
+
- If .env.mci files exist:
|
|
46
|
+
1. ./mci/.env.mci (library MCI-specific)
|
|
47
|
+
2. Project root .env.mci (project MCI-specific)
|
|
48
|
+
- If no .env.mci files exist:
|
|
49
|
+
1. ./mci/.env (library defaults)
|
|
50
|
+
2. Project root .env (project-level)
|
|
51
|
+
- Then:
|
|
52
|
+
3. System environment variables
|
|
53
|
+
4. env_vars argument (highest priority)
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
file_path: Path to the MCI schema file (.json, .yaml, or .yml)
|
|
57
|
+
env_vars: Optional environment variables for template substitution (highest priority)
|
|
58
|
+
auto_load_dotenv: Whether to automatically load .env files (default: True)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
An initialized MCIClient instance
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
MCIClientError: If the schema file cannot be loaded or parsed, or if
|
|
65
|
+
validation fails
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> config = MCIConfig()
|
|
69
|
+
>>> try:
|
|
70
|
+
... # Auto-loads .env files from project root and ./mci
|
|
71
|
+
... client = config.load("mci.json")
|
|
72
|
+
... tools = client.tools()
|
|
73
|
+
... except MCIClientError as e:
|
|
74
|
+
... print(f"Schema invalid: {e}")
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
# Determine project root from schema file location
|
|
78
|
+
project_root = Path(file_path).parent.resolve()
|
|
79
|
+
|
|
80
|
+
# Load environment variables with proper precedence
|
|
81
|
+
if auto_load_dotenv:
|
|
82
|
+
merged_env = get_env_with_dotenv(project_root, env_vars)
|
|
83
|
+
else:
|
|
84
|
+
# If auto-loading is disabled, just use provided env_vars
|
|
85
|
+
merged_env = env_vars or {}
|
|
86
|
+
|
|
87
|
+
client = MCIClient(schema_file_path=file_path, env_vars=merged_env)
|
|
88
|
+
return client
|
|
89
|
+
except MCIClientError:
|
|
90
|
+
# Re-raise with the original error message from mci-py
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def validate_schema(
|
|
95
|
+
file_path: str, env_vars: dict[str, str] | None = None, auto_load_dotenv: bool = True
|
|
96
|
+
) -> tuple[bool, str]:
|
|
97
|
+
"""
|
|
98
|
+
Validate an MCI schema file using MCIClient.
|
|
99
|
+
|
|
100
|
+
This method validates the schema using MCIClient in validation-only mode,
|
|
101
|
+
which skips template resolution for MCP servers and other runtime concerns.
|
|
102
|
+
MCIClient performs comprehensive validation including schema structure,
|
|
103
|
+
required fields, and data types.
|
|
104
|
+
|
|
105
|
+
If auto_load_dotenv is True (default), automatically loads environment variables
|
|
106
|
+
from .env files in the project root and ./mci directory.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file_path: Path to the MCI schema file to validate
|
|
110
|
+
env_vars: Optional environment variables for template substitution
|
|
111
|
+
auto_load_dotenv: Whether to automatically load .env files (default: True)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
A tuple of (is_valid, error_message) where:
|
|
115
|
+
- is_valid is True if the schema is valid, False otherwise
|
|
116
|
+
- error_message is empty string if valid, or contains error details if invalid
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> config = MCIConfig()
|
|
120
|
+
>>> is_valid, error = config.validate_schema("mci.json")
|
|
121
|
+
>>> if not is_valid:
|
|
122
|
+
... print(f"Validation failed: {error}")
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
# Determine project root from schema file location
|
|
126
|
+
project_root = Path(file_path).parent.resolve()
|
|
127
|
+
|
|
128
|
+
# Load environment variables with proper precedence
|
|
129
|
+
if auto_load_dotenv:
|
|
130
|
+
merged_env = get_env_with_dotenv(project_root, env_vars)
|
|
131
|
+
else:
|
|
132
|
+
# If auto-loading is disabled, just use provided env_vars
|
|
133
|
+
merged_env = env_vars or {}
|
|
134
|
+
|
|
135
|
+
# Use validating=True to skip template resolution for MCP servers
|
|
136
|
+
# This allows validation without requiring all env_vars at validation time
|
|
137
|
+
MCIClient(schema_file_path=file_path, env_vars=merged_env, validating=True)
|
|
138
|
+
return (True, "")
|
|
139
|
+
except MCIClientError as e:
|
|
140
|
+
return (False, str(e))
|
|
141
|
+
except FileNotFoundError:
|
|
142
|
+
return (False, f"File not found: {file_path}")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return (False, f"Unexpected error: {str(e)}")
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dynamic_server.py - Dynamic MCP server creation from MCI schemas
|
|
3
|
+
|
|
4
|
+
This module provides the DynamicMCPServer class for creating and running
|
|
5
|
+
MCP servers that dynamically load tools from MCI schema files. The server
|
|
6
|
+
uses MCIClient to load tools (including toolsets and environment variables),
|
|
7
|
+
converts them to MCP format, and delegates execution back to MCIClient.
|
|
8
|
+
|
|
9
|
+
The server supports:
|
|
10
|
+
- Loading tools from JSON/YAML MCI schemas
|
|
11
|
+
- Tool filtering by name, tags, and toolsets
|
|
12
|
+
- STDIO transport for MCP protocol
|
|
13
|
+
- Graceful shutdown and error handling
|
|
14
|
+
- Environment variable templating
|
|
15
|
+
|
|
16
|
+
Note: This module accesses dynamically added attributes on MCP Server instances
|
|
17
|
+
(prefixed with _mci_). Type checkers will report these as unknown attributes,
|
|
18
|
+
which is expected.
|
|
19
|
+
"""
|
|
20
|
+
# pyright: reportAttributeAccessIssue=false
|
|
21
|
+
|
|
22
|
+
from mcp.server.lowlevel import Server
|
|
23
|
+
|
|
24
|
+
from mci.core.mci_client import MCIClientWrapper
|
|
25
|
+
from mci.core.mcp_server import MCPServerBuilder, ServerInstance
|
|
26
|
+
from mci.core.tool_manager import ToolManager
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DynamicMCPServer:
|
|
30
|
+
"""
|
|
31
|
+
Creates and manages dynamic MCP servers from MCI schemas.
|
|
32
|
+
|
|
33
|
+
This class provides the main interface for creating MCP servers that serve
|
|
34
|
+
tools from MCI schema files. It handles loading, filtering, conversion,
|
|
35
|
+
and runtime management of the server.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
schema_path: str,
|
|
41
|
+
filter_spec: str | None = None,
|
|
42
|
+
env_vars: dict[str, str] | None = None,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the dynamic server configuration.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
schema_path: Path to the MCI schema file (.json, .yaml, or .yml)
|
|
49
|
+
filter_spec: Optional filter specification (e.g., "tags:api,database")
|
|
50
|
+
env_vars: Optional environment variables for template substitution
|
|
51
|
+
"""
|
|
52
|
+
self.schema_path: str = schema_path
|
|
53
|
+
self.filter_spec: str | None = filter_spec
|
|
54
|
+
self.env_vars: dict[str, str] = env_vars or {}
|
|
55
|
+
self.server: Server | None = None
|
|
56
|
+
self.instance: ServerInstance | None = None
|
|
57
|
+
self.mci_client_wrapper: MCIClientWrapper | None = None
|
|
58
|
+
|
|
59
|
+
async def create_from_mci_schema(
|
|
60
|
+
self, server_name: str = "mci-dynamic-server", server_version: str = "1.0.0"
|
|
61
|
+
) -> ServerInstance:
|
|
62
|
+
"""
|
|
63
|
+
Create an MCP server instance from the MCI schema.
|
|
64
|
+
|
|
65
|
+
This method:
|
|
66
|
+
1. Loads tools using MCIClient (with toolsets and env var templating)
|
|
67
|
+
2. Applies filters if specified
|
|
68
|
+
3. Converts tools to MCP format using Stage 8 converter
|
|
69
|
+
4. Registers tools with MCP server
|
|
70
|
+
5. Sets up handlers for tool listing and execution
|
|
71
|
+
6. Returns ServerInstance ready for STDIO transport
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
server_name: Name for the MCP server
|
|
75
|
+
server_version: Version string for the server
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ServerInstance configured with MCI tools
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
Exception: If schema loading, tool conversion, or server creation fails
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> server = DynamicMCPServer("mci.json")
|
|
85
|
+
>>> instance = await server.create_from_mci_schema()
|
|
86
|
+
>>> await instance.start(stdio=True)
|
|
87
|
+
"""
|
|
88
|
+
# Step 1: Load tools using MCIClient
|
|
89
|
+
self.mci_client_wrapper = MCIClientWrapper(self.schema_path, self.env_vars)
|
|
90
|
+
|
|
91
|
+
# Step 2: Apply filters if specified
|
|
92
|
+
if self.filter_spec:
|
|
93
|
+
tools = ToolManager.apply_filter_spec(self.mci_client_wrapper, self.filter_spec)
|
|
94
|
+
else:
|
|
95
|
+
tools = self.mci_client_wrapper.get_tools()
|
|
96
|
+
|
|
97
|
+
# Step 3: Create MCP server using Stage 8 infrastructure
|
|
98
|
+
builder = MCPServerBuilder(self.mci_client_wrapper.client)
|
|
99
|
+
self.server = await builder.create_server(server_name, server_version)
|
|
100
|
+
|
|
101
|
+
# Step 4: Register all tools (converter handles MCI to MCP conversion)
|
|
102
|
+
await builder.register_all_tools(self.server, tools)
|
|
103
|
+
|
|
104
|
+
# Step 5: Create ServerInstance (sets up handlers for listing and execution)
|
|
105
|
+
self.instance = ServerInstance(self.server, self.mci_client_wrapper.client, self.env_vars)
|
|
106
|
+
|
|
107
|
+
return self.instance
|
|
108
|
+
|
|
109
|
+
async def start_stdio(self) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Start the MCP server on STDIO transport.
|
|
112
|
+
|
|
113
|
+
This method starts the server and blocks until shutdown is requested.
|
|
114
|
+
The server will stop when:
|
|
115
|
+
- Ctrl+C (KeyboardInterrupt) is pressed
|
|
116
|
+
- The client disconnects
|
|
117
|
+
- An unrecoverable error occurs
|
|
118
|
+
|
|
119
|
+
The server will:
|
|
120
|
+
- Respond to MCP protocol requests for tool listing
|
|
121
|
+
- Execute tools by delegating to MCIClient.execute()
|
|
122
|
+
- Support structured output validation (outputSchema)
|
|
123
|
+
- Handle errors gracefully
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
RuntimeError: If server instance is not created
|
|
127
|
+
Exception: If server startup or runtime fails
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> server = DynamicMCPServer("mci.json")
|
|
131
|
+
>>> await server.create_from_mci_schema()
|
|
132
|
+
>>> await server.start_stdio() # Blocks until Ctrl+C
|
|
133
|
+
"""
|
|
134
|
+
if self.instance is None:
|
|
135
|
+
raise RuntimeError("Server instance not created. Call create_from_mci_schema() first.")
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
# Start the server on STDIO (this blocks until server stops)
|
|
139
|
+
# KeyboardInterrupt will be raised automatically when Ctrl+C is pressed
|
|
140
|
+
await self.instance.start(stdio=True)
|
|
141
|
+
except KeyboardInterrupt:
|
|
142
|
+
# Graceful shutdown on Ctrl+C
|
|
143
|
+
pass
|
|
144
|
+
finally:
|
|
145
|
+
# Cleanup
|
|
146
|
+
if self.instance:
|
|
147
|
+
self.instance.stop()
|
|
148
|
+
|
|
149
|
+
def get_tool_count(self) -> int:
|
|
150
|
+
"""
|
|
151
|
+
Get the number of tools registered with the server.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Number of registered tools, or 0 if server not created
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> server = DynamicMCPServer("mci.json")
|
|
158
|
+
>>> await server.create_from_mci_schema()
|
|
159
|
+
>>> count = server.get_tool_count()
|
|
160
|
+
>>> print(f"Server has {count} tools")
|
|
161
|
+
"""
|
|
162
|
+
if self.server and hasattr(self.server, "_mci_tools"):
|
|
163
|
+
return len(self.server._mci_tools) # type: ignore[attr-defined]
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def run_server(
|
|
168
|
+
schema_path: str, filter_spec: str | None = None, env_vars: dict[str, str] | None = None
|
|
169
|
+
) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Create and run a dynamic MCP server from an MCI schema.
|
|
172
|
+
|
|
173
|
+
This is a convenience function that creates a server, loads tools,
|
|
174
|
+
and starts it on STDIO in a single call.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
schema_path: Path to the MCI schema file
|
|
178
|
+
filter_spec: Optional filter specification (e.g., "tags:api,database")
|
|
179
|
+
env_vars: Optional environment variables for template substitution
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
>>> await run_server("mci.json")
|
|
183
|
+
>>> await run_server("mci.json", filter_spec="tags:api")
|
|
184
|
+
"""
|
|
185
|
+
server = DynamicMCPServer(schema_path, filter_spec, env_vars)
|
|
186
|
+
await server.create_from_mci_schema()
|
|
187
|
+
await server.start_stdio()
|
mci/core/file_finder.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file_finder.py - File discovery logic for MCI configuration files
|
|
3
|
+
|
|
4
|
+
This module provides functionality to locate MCI configuration files (mci.json or mci.yaml)
|
|
5
|
+
in a given directory. It prioritizes JSON files when both formats exist and provides
|
|
6
|
+
utilities for file format detection and validation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from mci.utils.validation import file_exists
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCIFileFinder:
|
|
15
|
+
"""
|
|
16
|
+
Handles discovery of MCI configuration files in directories.
|
|
17
|
+
|
|
18
|
+
This class provides methods to find MCI configuration files (mci.json or mci.yaml),
|
|
19
|
+
with JSON files taking priority when both formats exist. It also includes utilities
|
|
20
|
+
for file validation and format detection.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def find_mci_file(directory: str = ".") -> str | None:
|
|
25
|
+
"""
|
|
26
|
+
Find an MCI configuration file in the specified directory.
|
|
27
|
+
|
|
28
|
+
Searches for mci.json first, then mci.yaml/mci.yml if JSON is not found.
|
|
29
|
+
This prioritizes JSON format when both formats exist in the same directory.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
directory: The directory path to search in (default: current directory)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The absolute path to the found MCI file, or None if no file is found
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> finder = MCIFileFinder()
|
|
39
|
+
>>> path = finder.find_mci_file("./my_project")
|
|
40
|
+
>>> if path:
|
|
41
|
+
... print(f"Found: {path}")
|
|
42
|
+
"""
|
|
43
|
+
dir_path = Path(directory).resolve()
|
|
44
|
+
|
|
45
|
+
# Check for mci.json first (priority)
|
|
46
|
+
json_path = dir_path / "mci.json"
|
|
47
|
+
if json_path.exists() and json_path.is_file():
|
|
48
|
+
return str(json_path)
|
|
49
|
+
|
|
50
|
+
# Check for mci.yaml
|
|
51
|
+
yaml_path = dir_path / "mci.yaml"
|
|
52
|
+
if yaml_path.exists() and yaml_path.is_file():
|
|
53
|
+
return str(yaml_path)
|
|
54
|
+
|
|
55
|
+
# Check for mci.yml as well
|
|
56
|
+
yml_path = dir_path / "mci.yml"
|
|
57
|
+
if yml_path.exists() and yml_path.is_file():
|
|
58
|
+
return str(yml_path)
|
|
59
|
+
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def validate_file_exists(path: str) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Check if a file exists at the given path.
|
|
66
|
+
|
|
67
|
+
Delegates to the validation utility module to avoid code duplication.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
path: The file path to validate
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if the file exists and is a file, False otherwise
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> finder = MCIFileFinder()
|
|
77
|
+
>>> exists = finder.validate_file_exists("./mci.json")
|
|
78
|
+
"""
|
|
79
|
+
return file_exists(path)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def get_file_format(path: str) -> str | None:
|
|
83
|
+
"""
|
|
84
|
+
Determine the file format based on the file extension.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
path: The file path to check
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
"json" for .json files, "yaml" for .yaml/.yml files, None for unknown formats
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> finder = MCIFileFinder()
|
|
94
|
+
>>> fmt = finder.get_file_format("./mci.json")
|
|
95
|
+
>>> print(fmt) # Output: "json"
|
|
96
|
+
"""
|
|
97
|
+
file_path = Path(path)
|
|
98
|
+
ext = file_path.suffix.lower()
|
|
99
|
+
|
|
100
|
+
if ext == ".json":
|
|
101
|
+
return "json"
|
|
102
|
+
elif ext in [".yaml", ".yml"]:
|
|
103
|
+
return "yaml"
|
|
104
|
+
else:
|
|
105
|
+
return None
|