embed-client-dev 4.0.78__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.
- embed_client/__init__.py +36 -0
- embed_client/__main__.py +15 -0
- embed_client/_version.py +3 -0
- embed_client/adapter_config_factory.py +213 -0
- embed_client/adapter_config_normalizer.py +135 -0
- embed_client/adapter_generator_bridge.py +166 -0
- embed_client/auth.py +189 -0
- embed_client/cli.py +483 -0
- embed_client/cli_core.py +427 -0
- embed_client/client_factory.py +515 -0
- embed_client/client_factory_examples.py +375 -0
- embed_client/client_factory_helpers.py +59 -0
- embed_client/config.py +408 -0
- embed_client/config_defaults.py +77 -0
- embed_client/config_examples.py +228 -0
- embed_client/config_generator.py +652 -0
- embed_client/config_generator_cli.py +151 -0
- embed_client/config_presets.py +245 -0
- embed_client/config_validator.py +246 -0
- embed_client/config_validator_cli.py +141 -0
- embed_client/constants.py +52 -0
- embed_client/demo_cli.py +78 -0
- embed_client/demo_security_cli.py +206 -0
- embed_client/embed_file_output_validate.py +348 -0
- embed_client/embed_file_status_store.py +80 -0
- embed_client/embedding_client.py +1929 -0
- embed_client/embedding_client_helpers.py +105 -0
- embed_client/example_async_usage.py +357 -0
- embed_client/example_async_usage_demo.py +282 -0
- embed_client/example_async_usage_ru.py +340 -0
- embed_client/example_async_usage_ru_demo.py +313 -0
- embed_client/exceptions.py +122 -0
- embed_client/identifier_types.py +56 -0
- embed_client/json_embed_file_preflight.py +128 -0
- embed_client/local_json_embed_file.py +73 -0
- embed_client/md_embed_file_prep.py +197 -0
- embed_client/response_normalizer.py +160 -0
- embed_client/response_parsers.py +219 -0
- embed_client/security_cli.py +200 -0
- embed_client/security_cli_core.py +227 -0
- embed_client/ssl_examples.py +278 -0
- embed_client/ssl_manager.py +141 -0
- embed_client_dev-4.0.78.dist-info/METADATA +155 -0
- embed_client_dev-4.0.78.dist-info/RECORD +48 -0
- embed_client_dev-4.0.78.dist-info/WHEEL +5 -0
- embed_client_dev-4.0.78.dist-info/entry_points.txt +4 -0
- embed_client_dev-4.0.78.dist-info/licenses/LICENSE +21 -0
- embed_client_dev-4.0.78.dist-info/top_level.txt +1 -0
embed_client/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
embed-client: client for the Embedding Service API with authentication,
|
|
3
|
+
SSL/TLS, and mTLS configuration.
|
|
4
|
+
|
|
5
|
+
Canonical runtime client is :class:`EmbeddingClient` (direct JSON-RPC via
|
|
6
|
+
``mcp_proxy_adapter.client.jsonrpc_client.JsonRpcClient``).
|
|
7
|
+
|
|
8
|
+
Author: Vasiliy Zdanovskiy
|
|
9
|
+
email: vasilyvz@gmail.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from embed_client._version import __version__
|
|
13
|
+
|
|
14
|
+
from embed_client.auth import ClientAuthManager
|
|
15
|
+
from embed_client.config import ClientConfig
|
|
16
|
+
from embed_client.config_generator import ClientConfigGenerator
|
|
17
|
+
from embed_client.config_validator import ConfigValidator
|
|
18
|
+
from embed_client.embedding_client import EmbeddingClient
|
|
19
|
+
from embed_client.embedding_client_helpers import (
|
|
20
|
+
list_commands_via_client,
|
|
21
|
+
wait_for_job_poll,
|
|
22
|
+
)
|
|
23
|
+
from embed_client.ssl_manager import ClientSSLManager
|
|
24
|
+
from embed_client.client_factory import ClientFactory
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"EmbeddingClient",
|
|
28
|
+
"wait_for_job_poll",
|
|
29
|
+
"list_commands_via_client",
|
|
30
|
+
"ClientConfig",
|
|
31
|
+
"ClientAuthManager",
|
|
32
|
+
"ClientSSLManager",
|
|
33
|
+
"ClientFactory",
|
|
34
|
+
"ClientConfigGenerator",
|
|
35
|
+
"ConfigValidator",
|
|
36
|
+
]
|
embed_client/__main__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Main entry point for embed-client CLI.
|
|
4
|
+
|
|
5
|
+
Author: Vasiliy Zdanovskiy
|
|
6
|
+
email: vasilyvz@gmail.com
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from embed_client.cli import main
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
sys.exit(asyncio.run(main()))
|
embed_client/_version.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapter Configuration Factory
|
|
3
|
+
|
|
4
|
+
This module provides translation from ClientConfig to mcp_proxy_adapter JsonRpcClient parameters.
|
|
5
|
+
|
|
6
|
+
Author: Vasiliy Zdanovskiy
|
|
7
|
+
email: vasilyvz@gmail.com
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from embed_client.config import ClientConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AdapterConfigFactory:
|
|
19
|
+
"""
|
|
20
|
+
Factory for converting ClientConfig/config_dict to adapter JsonRpcClient parameters.
|
|
21
|
+
|
|
22
|
+
Translates embed-client configuration format to mcp_proxy_adapter JsonRpcClient
|
|
23
|
+
initialization parameters, handling all security modes and authentication methods.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def from_config_dict(config_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Convert config_dict to adapter parameters.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
config_dict: Configuration dictionary with server, auth, ssl sections
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dictionary with adapter parameters: protocol, host, port, token_header,
|
|
36
|
+
token, cert, key, ca, check_hostname, timeout
|
|
37
|
+
"""
|
|
38
|
+
server_config = config_dict.get("server", {})
|
|
39
|
+
auth_config = config_dict.get("auth", {})
|
|
40
|
+
ssl_config = config_dict.get("ssl", {})
|
|
41
|
+
client_config = config_dict.get("client", {})
|
|
42
|
+
|
|
43
|
+
# Extract base URL and parse it
|
|
44
|
+
base_url = server_config.get("base_url") or server_config.get(
|
|
45
|
+
"host", "http://localhost"
|
|
46
|
+
)
|
|
47
|
+
port = server_config.get("port", 8001)
|
|
48
|
+
|
|
49
|
+
# A bare host/IP (as the config generator writes it) has no scheme;
|
|
50
|
+
# give urlparse a netloc so the host is kept instead of dropped.
|
|
51
|
+
if "://" not in base_url:
|
|
52
|
+
base_url = f"http://{base_url}"
|
|
53
|
+
parsed = urlparse(base_url)
|
|
54
|
+
protocol = parsed.scheme or "http"
|
|
55
|
+
host = parsed.hostname or "localhost"
|
|
56
|
+
|
|
57
|
+
# If port is in URL, use it; otherwise use config port
|
|
58
|
+
if parsed.port:
|
|
59
|
+
port = parsed.port
|
|
60
|
+
elif ":" in base_url and not base_url.startswith("http"):
|
|
61
|
+
# Handle edge case where port might be in hostname
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# Determine protocol for adapter (http, https, mtls)
|
|
65
|
+
adapter_protocol = "http"
|
|
66
|
+
if protocol == "https" or ssl_config.get("enabled", False):
|
|
67
|
+
if ssl_config.get("cert_file") and ssl_config.get("key_file"):
|
|
68
|
+
adapter_protocol = "mtls"
|
|
69
|
+
else:
|
|
70
|
+
adapter_protocol = "https"
|
|
71
|
+
|
|
72
|
+
# Extract authentication parameters
|
|
73
|
+
token_header: Optional[str] = None
|
|
74
|
+
token: Optional[str] = None
|
|
75
|
+
auth_method = auth_config.get("method", "none")
|
|
76
|
+
|
|
77
|
+
if auth_method == "api_key":
|
|
78
|
+
api_keys = auth_config.get("api_keys", {})
|
|
79
|
+
if api_keys:
|
|
80
|
+
# Use first available API key
|
|
81
|
+
token = list(api_keys.values())[0]
|
|
82
|
+
token_header = auth_config.get("api_key_header", "X-API-Key")
|
|
83
|
+
elif auth_method == "jwt":
|
|
84
|
+
# JWT tokens are created dynamically, but we can set header
|
|
85
|
+
token_header = "Authorization"
|
|
86
|
+
# Token will be generated by auth manager
|
|
87
|
+
jwt_config = auth_config.get("jwt", {})
|
|
88
|
+
if jwt_config.get("username") and jwt_config.get("password"):
|
|
89
|
+
# Store credentials for later token generation
|
|
90
|
+
token = None # Will be generated on demand
|
|
91
|
+
elif auth_method == "basic":
|
|
92
|
+
token_header = "Authorization"
|
|
93
|
+
# Basic auth will be handled via adapter's token mechanism
|
|
94
|
+
basic_config = auth_config.get("basic", {})
|
|
95
|
+
if basic_config.get("username") and basic_config.get("password"):
|
|
96
|
+
token = None # Will be generated on demand
|
|
97
|
+
|
|
98
|
+
# Extract SSL/TLS parameters
|
|
99
|
+
cert: Optional[str] = None
|
|
100
|
+
key: Optional[str] = None
|
|
101
|
+
ca: Optional[str] = None
|
|
102
|
+
check_hostname = ssl_config.get("check_hostname", True)
|
|
103
|
+
|
|
104
|
+
if adapter_protocol in ("https", "mtls"):
|
|
105
|
+
cert = ssl_config.get("cert_file")
|
|
106
|
+
key = ssl_config.get("key_file")
|
|
107
|
+
ca = ssl_config.get("ca_cert_file")
|
|
108
|
+
|
|
109
|
+
# If mTLS, ensure cert and key are set
|
|
110
|
+
if adapter_protocol == "mtls" and not (cert and key):
|
|
111
|
+
# Fallback to auth certificate config
|
|
112
|
+
cert = auth_config.get("certificate", {}).get("cert_file")
|
|
113
|
+
key = auth_config.get("certificate", {}).get("key_file")
|
|
114
|
+
|
|
115
|
+
# Extract timeout
|
|
116
|
+
timeout = client_config.get("timeout", 30.0)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"protocol": adapter_protocol,
|
|
120
|
+
"host": host,
|
|
121
|
+
"port": port,
|
|
122
|
+
"token_header": token_header,
|
|
123
|
+
"token": token,
|
|
124
|
+
"cert": cert,
|
|
125
|
+
"key": key,
|
|
126
|
+
"ca": ca,
|
|
127
|
+
"check_hostname": check_hostname,
|
|
128
|
+
"timeout": timeout,
|
|
129
|
+
# Store original config for auth manager integration
|
|
130
|
+
"_original_config": config_dict,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def from_client_config(config: "ClientConfig") -> Dict[str, Any]:
|
|
135
|
+
"""
|
|
136
|
+
Convert ClientConfig object to adapter parameters.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
config: ClientConfig instance
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dictionary with adapter parameters
|
|
143
|
+
"""
|
|
144
|
+
return AdapterConfigFactory.from_config_dict(config.get_all())
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def from_legacy_params(
|
|
148
|
+
base_url: str,
|
|
149
|
+
port: Optional[int] = None,
|
|
150
|
+
auth_method: Optional[str] = None,
|
|
151
|
+
ssl_enabled: Optional[bool] = None,
|
|
152
|
+
**kwargs,
|
|
153
|
+
) -> Dict[str, Any]:
|
|
154
|
+
"""
|
|
155
|
+
Convert legacy factory parameters to adapter parameters.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
base_url: Server base URL
|
|
159
|
+
port: Server port
|
|
160
|
+
auth_method: Authentication method
|
|
161
|
+
ssl_enabled: Whether SSL is enabled
|
|
162
|
+
**kwargs: Additional parameters (cert_file, key_file, api_key, etc.)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dictionary with adapter parameters
|
|
166
|
+
"""
|
|
167
|
+
parsed = urlparse(base_url)
|
|
168
|
+
protocol = parsed.scheme or "http"
|
|
169
|
+
host = parsed.hostname or "localhost"
|
|
170
|
+
actual_port = port or parsed.port or (443 if protocol == "https" else 80)
|
|
171
|
+
|
|
172
|
+
# Determine protocol
|
|
173
|
+
adapter_protocol = "http"
|
|
174
|
+
if ssl_enabled or protocol == "https":
|
|
175
|
+
if kwargs.get("cert_file") and kwargs.get("key_file"):
|
|
176
|
+
adapter_protocol = "mtls"
|
|
177
|
+
else:
|
|
178
|
+
adapter_protocol = "https"
|
|
179
|
+
|
|
180
|
+
# Extract auth
|
|
181
|
+
token_header: Optional[str] = None
|
|
182
|
+
token: Optional[str] = None
|
|
183
|
+
|
|
184
|
+
if auth_method == "api_key":
|
|
185
|
+
token = kwargs.get("api_key") or os.environ.get("EMBED_CLIENT_API_KEY")
|
|
186
|
+
token_header = kwargs.get("api_key_header", "X-API-Key")
|
|
187
|
+
elif auth_method == "jwt":
|
|
188
|
+
token_header = "Authorization"
|
|
189
|
+
# JWT will be generated on demand
|
|
190
|
+
elif auth_method == "basic":
|
|
191
|
+
token_header = "Authorization"
|
|
192
|
+
# Basic auth will be generated on demand
|
|
193
|
+
|
|
194
|
+
# Extract SSL
|
|
195
|
+
cert = kwargs.get("cert_file")
|
|
196
|
+
key = kwargs.get("key_file")
|
|
197
|
+
ca = kwargs.get("ca_cert_file")
|
|
198
|
+
check_hostname = kwargs.get("check_hostname", True)
|
|
199
|
+
|
|
200
|
+
timeout = kwargs.get("timeout", 30.0)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
"protocol": adapter_protocol,
|
|
204
|
+
"host": host,
|
|
205
|
+
"port": actual_port,
|
|
206
|
+
"token_header": token_header,
|
|
207
|
+
"token": token,
|
|
208
|
+
"cert": cert,
|
|
209
|
+
"key": key,
|
|
210
|
+
"ca": ca,
|
|
211
|
+
"check_hostname": check_hostname,
|
|
212
|
+
"timeout": timeout,
|
|
213
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Normalize embed-client config to adapter config format for validation.
|
|
3
|
+
|
|
4
|
+
Converts client config (server.host, server.port, protocol, ssl, auth)
|
|
5
|
+
to the format expected by mcp_proxy_adapter.core.validation.config_validator,
|
|
6
|
+
and maps validation results back to embed-client section/key names.
|
|
7
|
+
|
|
8
|
+
Author: Vasiliy Zdanovskiy
|
|
9
|
+
email: vasilyvz@gmail.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
# Section/key mapping: adapter -> embed-client (for user-facing messages)
|
|
17
|
+
_ADAPTER_TO_CLIENT_KEY: Dict[str, str] = {
|
|
18
|
+
"server.ssl.cert": "ssl.cert_file",
|
|
19
|
+
"server.ssl.key": "ssl.key_file",
|
|
20
|
+
"server.ssl.ca": "ssl.ca_cert_file",
|
|
21
|
+
"server.ssl.crl": "ssl.crl_file",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def client_to_adapter_format(
|
|
26
|
+
config_data: Dict[str, Any],
|
|
27
|
+
config_path: Optional[str] = None,
|
|
28
|
+
) -> Dict[str, Any]:
|
|
29
|
+
"""
|
|
30
|
+
Convert embed-client config to adapter ConfigValidator input format.
|
|
31
|
+
|
|
32
|
+
Adapter expects: server.host, server.port, server.protocol, server.debug,
|
|
33
|
+
server.log_level, server.ssl.{cert,key,ca}, auth, optional ssl, etc.
|
|
34
|
+
"""
|
|
35
|
+
server = config_data.get("server", {})
|
|
36
|
+
protocol = config_data.get("protocol", server.get("protocol", "http"))
|
|
37
|
+
ssl_config = config_data.get("ssl", {})
|
|
38
|
+
auth_config = config_data.get("auth", {})
|
|
39
|
+
|
|
40
|
+
# Cert paths: embed-client uses ssl.cert_file / auth.certificate (keep as-is; adapter uses cwd for abspath)
|
|
41
|
+
cert_file = ssl_config.get("cert_file") or (
|
|
42
|
+
auth_config.get("certificate") or {}
|
|
43
|
+
).get("cert_file")
|
|
44
|
+
key_file = ssl_config.get("key_file") or (auth_config.get("certificate") or {}).get(
|
|
45
|
+
"key_file"
|
|
46
|
+
)
|
|
47
|
+
ca_cert_file = ssl_config.get("ca_cert_file") or (
|
|
48
|
+
auth_config.get("certificate") or {}
|
|
49
|
+
).get("ca_cert_file")
|
|
50
|
+
crl_file = ssl_config.get("crl_file")
|
|
51
|
+
|
|
52
|
+
server_ssl: Optional[Dict[str, Any]] = None
|
|
53
|
+
if protocol in ("https", "mtls") and (cert_file or key_file or ca_cert_file):
|
|
54
|
+
server_ssl = {
|
|
55
|
+
"cert": cert_file,
|
|
56
|
+
"key": key_file,
|
|
57
|
+
"ca": ca_cert_file,
|
|
58
|
+
"crl": crl_file,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
adapter_server: Dict[str, Any] = {
|
|
62
|
+
"host": server.get("host", "localhost"),
|
|
63
|
+
"port": server.get("port", 8001),
|
|
64
|
+
"protocol": protocol,
|
|
65
|
+
"debug": False,
|
|
66
|
+
"log_level": "INFO",
|
|
67
|
+
"log_dir": server.get("log_dir", "./logs"),
|
|
68
|
+
}
|
|
69
|
+
if server_ssl:
|
|
70
|
+
adapter_server["ssl"] = server_ssl
|
|
71
|
+
|
|
72
|
+
# Adapter auth: use_token, tokens, roles (for validation consistency)
|
|
73
|
+
security = config_data.get("security", {})
|
|
74
|
+
adapter_auth = {
|
|
75
|
+
"use_token": auth_config.get("method") == "api_key",
|
|
76
|
+
"use_roles": bool(security.get("roles")),
|
|
77
|
+
"tokens": {},
|
|
78
|
+
"roles": (
|
|
79
|
+
security.get("roles") if isinstance(security.get("roles"), dict) else {}
|
|
80
|
+
),
|
|
81
|
+
}
|
|
82
|
+
api_key = auth_config.get("api_key") or {}
|
|
83
|
+
if api_key.get("key"):
|
|
84
|
+
adapter_auth["tokens"] = {api_key["key"]: ["read", "write"]}
|
|
85
|
+
if security.get("tokens") and isinstance(security["tokens"], dict):
|
|
86
|
+
adapter_auth["tokens"] = {
|
|
87
|
+
(v if isinstance(v, str) else str(v)): ["read", "write"]
|
|
88
|
+
for v in security["tokens"].values()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
normalized: Dict[str, Any] = {
|
|
92
|
+
"server": adapter_server,
|
|
93
|
+
"auth": adapter_auth,
|
|
94
|
+
}
|
|
95
|
+
if ssl_config:
|
|
96
|
+
normalized["ssl"] = {
|
|
97
|
+
"enabled": ssl_config.get("enabled", protocol in ("https", "mtls")),
|
|
98
|
+
"cert_file": cert_file,
|
|
99
|
+
"key_file": key_file,
|
|
100
|
+
"ca_cert_file": ca_cert_file,
|
|
101
|
+
"crl_file": crl_file,
|
|
102
|
+
}
|
|
103
|
+
# Minimal sections to avoid "unknown section" warnings
|
|
104
|
+
normalized["client"] = {"enabled": False}
|
|
105
|
+
normalized["registration"] = {"enabled": False}
|
|
106
|
+
return normalized
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def map_adapter_errors_to_client(
|
|
110
|
+
adapter_errors: List[Any],
|
|
111
|
+
) -> List[str]:
|
|
112
|
+
"""
|
|
113
|
+
Map adapter ValidationResult list to embed-client error strings.
|
|
114
|
+
|
|
115
|
+
Translates section/key (e.g. server.ssl.cert) to client names (ssl.cert_file).
|
|
116
|
+
"""
|
|
117
|
+
from mcp_proxy_adapter.core.validation.validation_result import ValidationResult # type: ignore[import-untyped]
|
|
118
|
+
|
|
119
|
+
result: List[str] = []
|
|
120
|
+
for r in adapter_errors:
|
|
121
|
+
if not isinstance(r, ValidationResult):
|
|
122
|
+
result.append(str(r))
|
|
123
|
+
continue
|
|
124
|
+
if getattr(r, "level", "error") != "error":
|
|
125
|
+
continue
|
|
126
|
+
msg = getattr(r, "message", str(r))
|
|
127
|
+
section = getattr(r, "section", None)
|
|
128
|
+
key = getattr(r, "key", None)
|
|
129
|
+
if section and key:
|
|
130
|
+
full_key = f"{section}.{key}" if key else section
|
|
131
|
+
client_key = _ADAPTER_TO_CLIENT_KEY.get(full_key, full_key)
|
|
132
|
+
result.append(f"{client_key}: {msg}")
|
|
133
|
+
else:
|
|
134
|
+
result.append(msg)
|
|
135
|
+
return result
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bridge to mcp_proxy_adapter config generator.
|
|
3
|
+
|
|
4
|
+
Uses adapter SimpleConfigGenerator as base: generates adapter config then
|
|
5
|
+
converts to embed-client client config format (server.host/port, protocol,
|
|
6
|
+
ssl.cert_file/key_file/ca_cert_file, auth, client.timeout).
|
|
7
|
+
|
|
8
|
+
Author: Vasiliy Zdanovskiy
|
|
9
|
+
email: vasilyvz@gmail.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import tempfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from mcp_proxy_adapter.core.config.simple_config_generator import ( # type: ignore[import-untyped]
|
|
21
|
+
SimpleConfigGenerator,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_ADAPTER_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
_ADAPTER_AVAILABLE = False
|
|
27
|
+
SimpleConfigGenerator = None # type: ignore[misc, assignment]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def adapter_output_to_client_config(adapter_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Convert adapter generator output to embed-client config format.
|
|
33
|
+
|
|
34
|
+
Adapter has: server.{host, port, protocol, ssl.{cert, key, ca, crl}},
|
|
35
|
+
auth.{use_token, tokens, roles}. We output: server.{host, port},
|
|
36
|
+
protocol, ssl.{enabled, cert_file, key_file, ca_cert_file, crl_file},
|
|
37
|
+
auth.{method, api_key, certificate}, client.timeout, security.
|
|
38
|
+
"""
|
|
39
|
+
server = adapter_dict.get("server", {})
|
|
40
|
+
ssl = server.get("ssl") or {}
|
|
41
|
+
auth = adapter_dict.get("auth", {})
|
|
42
|
+
|
|
43
|
+
protocol = server.get("protocol", "http")
|
|
44
|
+
ssl_enabled = protocol in ("https", "mtls")
|
|
45
|
+
verify_mode = "CERT_REQUIRED" if protocol == "mtls" else "CERT_NONE"
|
|
46
|
+
|
|
47
|
+
# Build embed-client auth: certificate for mTLS, api_key when use_token
|
|
48
|
+
if protocol == "mtls":
|
|
49
|
+
auth_method = "certificate"
|
|
50
|
+
elif auth.get("use_token"):
|
|
51
|
+
auth_method = "api_key"
|
|
52
|
+
else:
|
|
53
|
+
auth_method = "none"
|
|
54
|
+
tokens = auth.get("tokens") or {}
|
|
55
|
+
first_token: Optional[str] = None
|
|
56
|
+
if isinstance(tokens, dict) and tokens:
|
|
57
|
+
first_token = next(iter(tokens.keys()), None)
|
|
58
|
+
|
|
59
|
+
config: Dict[str, Any] = {
|
|
60
|
+
"server": {
|
|
61
|
+
"host": server.get("host", "localhost"),
|
|
62
|
+
"port": server.get("port", 8001),
|
|
63
|
+
},
|
|
64
|
+
"client": {"timeout": 30.0},
|
|
65
|
+
"protocol": protocol,
|
|
66
|
+
"auth": {
|
|
67
|
+
"method": auth_method,
|
|
68
|
+
"api_key": {"key": first_token, "header": "X-API-Key"},
|
|
69
|
+
"certificate": {
|
|
70
|
+
"enabled": protocol == "mtls",
|
|
71
|
+
"cert_file": ssl.get("cert") if protocol == "mtls" else None,
|
|
72
|
+
"key_file": ssl.get("key") if protocol == "mtls" else None,
|
|
73
|
+
"ca_cert_file": ssl.get("ca") if protocol == "mtls" else None,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
"ssl": {
|
|
77
|
+
"enabled": ssl_enabled,
|
|
78
|
+
"verify": protocol != "http",
|
|
79
|
+
"verify_mode": verify_mode,
|
|
80
|
+
"check_hostname": False,
|
|
81
|
+
"check_expiry": False,
|
|
82
|
+
"cert_file": ssl.get("cert"),
|
|
83
|
+
"key_file": ssl.get("key"),
|
|
84
|
+
"ca_cert_file": ssl.get("ca"),
|
|
85
|
+
"crl_file": ssl.get("crl"),
|
|
86
|
+
"client_cert_required": protocol == "mtls",
|
|
87
|
+
},
|
|
88
|
+
"security": {"enabled": bool(auth.get("use_token")), "tokens": {}, "roles": {}},
|
|
89
|
+
}
|
|
90
|
+
if auth.get("use_token") and isinstance(tokens, dict) and tokens:
|
|
91
|
+
# embed-client security.tokens: role_name -> token_value
|
|
92
|
+
config["security"]["tokens"] = (
|
|
93
|
+
{"user": first_token} if first_token else dict(tokens)
|
|
94
|
+
)
|
|
95
|
+
if auth.get("use_roles") and isinstance(auth.get("roles"), dict):
|
|
96
|
+
config["security"]["roles"] = auth["roles"]
|
|
97
|
+
return config
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def generate_client_config_via_adapter(
|
|
101
|
+
protocol: str,
|
|
102
|
+
host: str = "localhost",
|
|
103
|
+
port: int = 8001,
|
|
104
|
+
cert_file: Optional[str] = None,
|
|
105
|
+
key_file: Optional[str] = None,
|
|
106
|
+
ca_cert_file: Optional[str] = None,
|
|
107
|
+
crl_file: Optional[str] = None,
|
|
108
|
+
token: Optional[str] = None,
|
|
109
|
+
use_token: bool = False,
|
|
110
|
+
use_roles: bool = False,
|
|
111
|
+
tokens: Optional[Dict[str, Any]] = None,
|
|
112
|
+
roles: Optional[Dict[str, Any]] = None,
|
|
113
|
+
output_path: Optional[Path] = None,
|
|
114
|
+
) -> Dict[str, Any]:
|
|
115
|
+
"""
|
|
116
|
+
Generate embed-client config using adapter SimpleConfigGenerator as base.
|
|
117
|
+
|
|
118
|
+
Calls adapter generator, loads output, converts to embed-client format.
|
|
119
|
+
Returns config dict; if output_path given, also saves JSON.
|
|
120
|
+
"""
|
|
121
|
+
if not _ADAPTER_AVAILABLE or SimpleConfigGenerator is None:
|
|
122
|
+
raise RuntimeError(
|
|
123
|
+
"mcp_proxy_adapter is not available; install it to use adapter-based generator"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if protocol == "mtls" and not ca_cert_file:
|
|
127
|
+
raise ValueError("CA certificate is required for mTLS. Provide ca_cert_file.")
|
|
128
|
+
|
|
129
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
|
|
130
|
+
temp_path = tmp.name
|
|
131
|
+
try:
|
|
132
|
+
gen = SimpleConfigGenerator()
|
|
133
|
+
gen.generate(
|
|
134
|
+
protocol=protocol,
|
|
135
|
+
with_proxy=False,
|
|
136
|
+
out_path=temp_path,
|
|
137
|
+
server_host=host,
|
|
138
|
+
server_port=port,
|
|
139
|
+
server_cert_file=cert_file,
|
|
140
|
+
server_key_file=key_file,
|
|
141
|
+
server_ca_cert_file=ca_cert_file,
|
|
142
|
+
server_crl_file=crl_file,
|
|
143
|
+
)
|
|
144
|
+
with open(temp_path, "r", encoding="utf-8") as f:
|
|
145
|
+
adapter_dict = json.load(f)
|
|
146
|
+
|
|
147
|
+
# Optionally set auth from adapter CLI-style (tokens/roles)
|
|
148
|
+
if use_token and (token or tokens):
|
|
149
|
+
adapter_dict.setdefault("auth", {})["use_token"] = True
|
|
150
|
+
if tokens:
|
|
151
|
+
adapter_dict["auth"]["tokens"] = tokens
|
|
152
|
+
elif token:
|
|
153
|
+
adapter_dict["auth"]["tokens"] = {token: ["read", "write"]}
|
|
154
|
+
if use_roles and roles:
|
|
155
|
+
adapter_dict.setdefault("auth", {})["use_roles"] = True
|
|
156
|
+
adapter_dict["auth"]["roles"] = roles
|
|
157
|
+
|
|
158
|
+
client_config = adapter_output_to_client_config(adapter_dict)
|
|
159
|
+
if output_path:
|
|
160
|
+
output_path = Path(output_path)
|
|
161
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
163
|
+
json.dump(client_config, f, indent=2, ensure_ascii=False)
|
|
164
|
+
return client_config
|
|
165
|
+
finally:
|
|
166
|
+
Path(temp_path).unlink(missing_ok=True)
|