axmp-openapi-fastmcp-server 0.3.0__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.
- axmp_openapi_fastmcp_server/__init__.py +19 -0
- axmp_openapi_fastmcp_server/__main__.py +7 -0
- axmp_openapi_fastmcp_server/auth/providers/keycloak.py +320 -0
- axmp_openapi_fastmcp_server/multi_openapi_spec.py +185 -0
- axmp_openapi_fastmcp_server/openapi_fastmcp_server.py +359 -0
- axmp_openapi_fastmcp_server/runner/cli.py +148 -0
- axmp_openapi_fastmcp_server/runner/container.py +68 -0
- axmp_openapi_fastmcp_server/settings.py +56 -0
- axmp_openapi_fastmcp_server/types.py +47 -0
- axmp_openapi_fastmcp_server-0.3.0.dist-info/METADATA +568 -0
- axmp_openapi_fastmcp_server-0.3.0.dist-info/RECORD +13 -0
- axmp_openapi_fastmcp_server-0.3.0.dist-info/WHEEL +4 -0
- axmp_openapi_fastmcp_server-0.3.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""This is the MCP server for the ZMP OpenAPI."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
# for stdio
|
|
6
|
+
logging.basicConfig(
|
|
7
|
+
level=logging.DEBUG,
|
|
8
|
+
format="[%(asctime)s.%(msecs)d] [%(levelname)s] [%(name)s] [%(threadName)s:%(thread)d] [%(module)s:%(funcName)s:%(lineno)d] - %(message)s",
|
|
9
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
10
|
+
handlers=[logging.StreamHandler()],
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# for streamable-http
|
|
14
|
+
# logging.config.fileConfig("logging.conf", disable_existing_loggers=False)
|
|
15
|
+
|
|
16
|
+
logging.getLogger("httpcore.http11").setLevel(logging.INFO)
|
|
17
|
+
logging.getLogger("sse_starlette.sse").setLevel(logging.INFO)
|
|
18
|
+
# logging.getLogger("axmp_openapi_helper").setLevel(logging.DEBUG)
|
|
19
|
+
# logging.getLogger("axmp_openapi_mcp_server.openapi_mcp_server").setLevel(logging.DEBUG)
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Keycloak OAuth provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides a complete Keycloak OAuth integration that's ready to use
|
|
4
|
+
with just a client ID and client secret. It handles all the complexity of
|
|
5
|
+
Keycloak's OAuth flow, token validation, and user management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from fastmcp.server.auth import TokenVerifier
|
|
14
|
+
from fastmcp.server.auth.auth import AccessToken
|
|
15
|
+
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
16
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
17
|
+
from fastmcp.utilities.logging import get_logger
|
|
18
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
19
|
+
from pydantic import AnyHttpUrl, BaseModel, Field, model_validator
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class KeyCloakEndpoints(BaseModel):
|
|
25
|
+
"""Keycloak endpoints."""
|
|
26
|
+
|
|
27
|
+
issuer_url: AnyHttpUrl | str = Field(...)
|
|
28
|
+
token_endpoint: AnyHttpUrl | str | None = None
|
|
29
|
+
authorization_endpoint: AnyHttpUrl | str | None = None
|
|
30
|
+
user_info_endpoint: AnyHttpUrl | str | None = None
|
|
31
|
+
jwks_uri: AnyHttpUrl | str | None = None
|
|
32
|
+
introspection_endpoint: AnyHttpUrl | str | None = None
|
|
33
|
+
revocation_endpoint: AnyHttpUrl | str | None = None
|
|
34
|
+
|
|
35
|
+
@model_validator(mode="after")
|
|
36
|
+
def set_defaults_from_issuer_url(self) -> KeyCloakEndpoints:
|
|
37
|
+
"""Set default endpoint URLs based on the actual issuer_url value."""
|
|
38
|
+
base = str(self.issuer_url).rstrip("/")
|
|
39
|
+
if self.token_endpoint is None:
|
|
40
|
+
self.token_endpoint = f"{base}/protocol/openid-connect/token"
|
|
41
|
+
if self.authorization_endpoint is None:
|
|
42
|
+
self.authorization_endpoint = f"{base}/protocol/openid-connect/auth"
|
|
43
|
+
if self.user_info_endpoint is None:
|
|
44
|
+
self.user_info_endpoint = f"{base}/protocol/openid-connect/userinfo"
|
|
45
|
+
if self.jwks_uri is None:
|
|
46
|
+
self.jwks_uri = f"{base}/protocol/openid-connect/certs"
|
|
47
|
+
if self.introspection_endpoint is None:
|
|
48
|
+
self.introspection_endpoint = (
|
|
49
|
+
f"{base}/protocol/openid-connect/token/introspect"
|
|
50
|
+
)
|
|
51
|
+
if self.revocation_endpoint is None:
|
|
52
|
+
self.revocation_endpoint = (
|
|
53
|
+
f"{base}/protocol/openid-connect/token/revocation"
|
|
54
|
+
)
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class KeyCloakTokenVerifier(TokenVerifier):
|
|
59
|
+
"""Token verifier for Keycloak OAuth tokens.
|
|
60
|
+
|
|
61
|
+
Keycloak OAuth tokens are opaque (not JWTs), so we verify them
|
|
62
|
+
by calling Keycloak's tokeninfo API to check if they're valid and get user info.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
issuer_url: AnyHttpUrl | str,
|
|
69
|
+
client_id: str,
|
|
70
|
+
client_secret: str,
|
|
71
|
+
required_scopes: list[str] | None = None,
|
|
72
|
+
timeout_seconds: int = 10,
|
|
73
|
+
):
|
|
74
|
+
"""Initialize the Keycloak token verifier.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
client_id: Client ID for introspection authentication
|
|
78
|
+
client_secret: Client secret for introspection authentication
|
|
79
|
+
required_scopes: Required OAuth scopes (e.g., ['openid', 'https://www.googleapis.com/auth/userinfo.email'])
|
|
80
|
+
timeout_seconds: HTTP request timeout
|
|
81
|
+
"""
|
|
82
|
+
super().__init__(required_scopes=required_scopes)
|
|
83
|
+
self.client_id = client_id
|
|
84
|
+
self.client_secret = client_secret
|
|
85
|
+
self.timeout_seconds = timeout_seconds
|
|
86
|
+
self.keycloak_endpoints = KeyCloakEndpoints(issuer_url=issuer_url)
|
|
87
|
+
|
|
88
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
89
|
+
"""Verify Keycloak OAuth token by calling Keycloak's tokeninfo API."""
|
|
90
|
+
try:
|
|
91
|
+
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
|
92
|
+
# Use Keycloak's introspection endpoint to validate the token
|
|
93
|
+
# Keycloak expects POST with basic auth for confidential clients
|
|
94
|
+
response = await client.post(
|
|
95
|
+
self.keycloak_endpoints.introspection_endpoint,
|
|
96
|
+
data={"token": token},
|
|
97
|
+
auth=(self.client_id, self.client_secret),
|
|
98
|
+
headers={"User-Agent": "FastMCP-Keycloak-OAuth"},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if response.status_code != 200:
|
|
102
|
+
logger.debug(
|
|
103
|
+
"Keycloak token verification failed: %d",
|
|
104
|
+
response.status_code,
|
|
105
|
+
)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
token_info = response.json()
|
|
109
|
+
|
|
110
|
+
# Check if token is active
|
|
111
|
+
if not token_info.get("active"):
|
|
112
|
+
logger.debug("Token is inactive")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
# Check if token is expired (backup check)
|
|
116
|
+
expires_in = token_info.get("expires_in")
|
|
117
|
+
if expires_in and int(expires_in) <= 0:
|
|
118
|
+
logger.debug("Access token has expired")
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
# Extract scopes from token info
|
|
122
|
+
scope_string = token_info.get("scope", "")
|
|
123
|
+
token_scopes = [
|
|
124
|
+
scope.strip() for scope in scope_string.split(" ") if scope.strip()
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
# Check required scopes
|
|
128
|
+
if self.required_scopes:
|
|
129
|
+
token_scopes_set = set(token_scopes)
|
|
130
|
+
required_scopes_set = set(self.required_scopes)
|
|
131
|
+
if not required_scopes_set.issubset(token_scopes_set):
|
|
132
|
+
logger.debug(
|
|
133
|
+
"Access token missing required scopes. Has %d, needs %d",
|
|
134
|
+
len(token_scopes_set),
|
|
135
|
+
len(required_scopes_set),
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
# Get additional user info if we have the right scopes
|
|
140
|
+
user_data = {}
|
|
141
|
+
if "openid" in token_scopes or "profile" in token_scopes:
|
|
142
|
+
try:
|
|
143
|
+
userinfo_response = await client.get(
|
|
144
|
+
self.keycloak_endpoints.user_info_endpoint,
|
|
145
|
+
headers={
|
|
146
|
+
"Authorization": f"Bearer {token}",
|
|
147
|
+
"User-Agent": "FastMCP-Keycloak-OAuth",
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
if userinfo_response.status_code == 200:
|
|
151
|
+
user_data = userinfo_response.json()
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.debug("Failed to fetch Keycloak user info: %s", e)
|
|
154
|
+
|
|
155
|
+
# Calculate expiration time
|
|
156
|
+
expires_at = None
|
|
157
|
+
if expires_in:
|
|
158
|
+
expires_at = int(time.time() + int(expires_in))
|
|
159
|
+
|
|
160
|
+
# Create AccessToken with Keycloak user info
|
|
161
|
+
access_token = AccessToken(
|
|
162
|
+
token=token,
|
|
163
|
+
client_id=token_info.get(
|
|
164
|
+
"audience", "unknown"
|
|
165
|
+
), # Use audience as client_id
|
|
166
|
+
scopes=token_scopes,
|
|
167
|
+
expires_at=expires_at,
|
|
168
|
+
claims={
|
|
169
|
+
"sub": user_data.get("sub"),
|
|
170
|
+
"email": user_data.get("email"),
|
|
171
|
+
"username": user_data.get("preferred_username")
|
|
172
|
+
if user_data.get("preferred_username")
|
|
173
|
+
else user_data.get("email"),
|
|
174
|
+
"given_name": user_data.get("given_name"),
|
|
175
|
+
"family_name": user_data.get("family_name"),
|
|
176
|
+
"keycloak_user_data": user_data,
|
|
177
|
+
"keycloak_token_info": token_info,
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
logger.debug("Keycloak token verified successfully")
|
|
181
|
+
return access_token
|
|
182
|
+
|
|
183
|
+
except httpx.RequestError as e:
|
|
184
|
+
logger.debug("Failed to verify Keycloak token: %s", e)
|
|
185
|
+
return None
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.debug("Keycloak token verification error: %s", e)
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class KeyCloakProvider(OAuthProxy):
|
|
192
|
+
"""Complete Keycloak OAuth provider for FastMCP.
|
|
193
|
+
|
|
194
|
+
This provider makes it trivial to add Keycloak OAuth protection to any
|
|
195
|
+
FastMCP server. Just provide your Keycloak OAuth app credentials and
|
|
196
|
+
a base URL, and you're ready to go.
|
|
197
|
+
|
|
198
|
+
Features:
|
|
199
|
+
- Transparent OAuth proxy to Keycloak
|
|
200
|
+
- Automatic token validation via Keycloak's tokeninfo API
|
|
201
|
+
- User information extraction from Keycloak APIs
|
|
202
|
+
- Minimal configuration required
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
```python
|
|
206
|
+
from fastmcp import FastMCP
|
|
207
|
+
from fastmcp.server.auth.providers.keycloak import KeycloakProvider
|
|
208
|
+
|
|
209
|
+
auth = KeycloakProvider(
|
|
210
|
+
client_id="123456789.apps.googleusercontent.com",
|
|
211
|
+
client_secret="GOCSPX-abc123...",
|
|
212
|
+
base_url="https://my-server.com"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
mcp = FastMCP("My App", auth=auth)
|
|
216
|
+
```
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
*,
|
|
222
|
+
client_id: str,
|
|
223
|
+
client_secret: str,
|
|
224
|
+
base_url: AnyHttpUrl | str,
|
|
225
|
+
issuer_url: AnyHttpUrl | str,
|
|
226
|
+
redirect_path: str | None = None,
|
|
227
|
+
required_scopes: list[str] | None = None,
|
|
228
|
+
timeout_seconds: int = 10,
|
|
229
|
+
allowed_client_redirect_uris: list[str] | None = None,
|
|
230
|
+
client_storage: AsyncKeyValue | None = None,
|
|
231
|
+
jwt_signing_key: str | bytes | None = None,
|
|
232
|
+
require_authorization_consent: bool = True,
|
|
233
|
+
extra_authorize_params: dict[str, str] | None = None,
|
|
234
|
+
):
|
|
235
|
+
"""Initialize KeyCloak OAuth provider.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
client_id: KeyCloak OAuth client ID (e.g., "123456789.apps.googleusercontent.com")
|
|
239
|
+
client_secret: KeyCloak OAuth client secret (e.g., "GOCSPX-abc123...")
|
|
240
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
241
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
242
|
+
to avoid 404s during discovery when mounting under a path.
|
|
243
|
+
redirect_path: Redirect path configured in KeyCloak OAuth app (defaults to "/auth/callback")
|
|
244
|
+
required_scopes: Required KeyCloak scopes (defaults to ["openid"]). Common scopes include:
|
|
245
|
+
- "openid" for OpenID Connect (default)
|
|
246
|
+
- "https://www.googleapis.com/auth/userinfo.email" for email access
|
|
247
|
+
- "https://www.googleapis.com/auth/userinfo.profile" for profile info
|
|
248
|
+
timeout_seconds: HTTP request timeout for KeyCloak API calls (defaults to 10)
|
|
249
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
250
|
+
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
251
|
+
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
|
|
252
|
+
If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
|
|
253
|
+
disk store will be encrypted using a key derived from the JWT Signing Key.
|
|
254
|
+
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
|
|
255
|
+
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
|
|
256
|
+
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
|
|
257
|
+
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
|
258
|
+
When True, users see a consent screen before being redirected to KeyCloak.
|
|
259
|
+
When False, authorization proceeds directly without user confirmation.
|
|
260
|
+
SECURITY WARNING: Only disable for local development or testing environments.
|
|
261
|
+
extra_authorize_params: Additional parameters to forward to KeyCloak's authorization endpoint.
|
|
262
|
+
By default, KeyCloakProvider sets {"access_type": "offline", "prompt": "consent"} to ensure
|
|
263
|
+
refresh tokens are returned. You can override these defaults or add additional parameters.
|
|
264
|
+
Example: {"prompt": "select_account"} to let users choose their KeyCloak account.
|
|
265
|
+
"""
|
|
266
|
+
# Parse scopes if provided as string
|
|
267
|
+
# KeyCloak requires at least one scope - openid is the minimal OIDC scope
|
|
268
|
+
required_scopes_final = (
|
|
269
|
+
parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Create Keycloak token verifier
|
|
273
|
+
token_verifier = KeyCloakTokenVerifier(
|
|
274
|
+
issuer_url=issuer_url,
|
|
275
|
+
client_id=client_id,
|
|
276
|
+
client_secret=client_secret,
|
|
277
|
+
required_scopes=required_scopes_final,
|
|
278
|
+
timeout_seconds=timeout_seconds,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Get Keycloak endpoints
|
|
282
|
+
keycloak_endpoints = KeyCloakEndpoints(
|
|
283
|
+
issuer_url=issuer_url,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Set Keycloak-specific defaults for extra authorize params
|
|
287
|
+
# access_type=offline ensures refresh tokens are returned
|
|
288
|
+
# prompt=consent forces consent screen to get refresh token (KeyCloak only issues on first auth otherwise)
|
|
289
|
+
# keycloak_defaults = {
|
|
290
|
+
# "access_type": "offline",
|
|
291
|
+
# "prompt": "consent",
|
|
292
|
+
# }
|
|
293
|
+
# User-provided params override defaults
|
|
294
|
+
# if extra_authorize_params:
|
|
295
|
+
# keycloak_defaults.update(extra_authorize_params)
|
|
296
|
+
# extra_authorize_params_final = keycloak_defaults
|
|
297
|
+
|
|
298
|
+
# Initialize OAuth proxy with KeyCloak endpoints
|
|
299
|
+
super().__init__(
|
|
300
|
+
upstream_authorization_endpoint=keycloak_endpoints.authorization_endpoint,
|
|
301
|
+
upstream_token_endpoint=keycloak_endpoints.token_endpoint,
|
|
302
|
+
upstream_client_id=client_id,
|
|
303
|
+
upstream_client_secret=client_secret,
|
|
304
|
+
token_verifier=token_verifier,
|
|
305
|
+
base_url=base_url,
|
|
306
|
+
redirect_path=redirect_path,
|
|
307
|
+
# issuer_url=issuer_url or base_url, # Default to base_url if not specified
|
|
308
|
+
issuer_url=base_url, # NOTE: should be the same as base_url for the DCR
|
|
309
|
+
allowed_client_redirect_uris=allowed_client_redirect_uris,
|
|
310
|
+
client_storage=client_storage,
|
|
311
|
+
jwt_signing_key=jwt_signing_key,
|
|
312
|
+
require_authorization_consent=require_authorization_consent,
|
|
313
|
+
extra_authorize_params=extra_authorize_params,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
logger.debug(
|
|
317
|
+
"Initialized Keycloak OAuth provider for client %s with scopes: %s",
|
|
318
|
+
client_id,
|
|
319
|
+
required_scopes_final,
|
|
320
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""This module provides a model for the mixed API specification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthenticationType(str, Enum):
|
|
13
|
+
"""Authentication type model."""
|
|
14
|
+
|
|
15
|
+
NONE = "None"
|
|
16
|
+
BASIC = "Basic"
|
|
17
|
+
BEARER_TOKEN = "BearerToken"
|
|
18
|
+
API_KEY = "ApiKey"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MethodSpec(BaseModel):
|
|
22
|
+
"""A model for the method specification."""
|
|
23
|
+
|
|
24
|
+
method: str
|
|
25
|
+
tool_name: str | None = None
|
|
26
|
+
description: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class APIMap(BaseModel):
|
|
30
|
+
"""A model for the API simple specification."""
|
|
31
|
+
|
|
32
|
+
path: str
|
|
33
|
+
methods: List[str | MethodSpec]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RouteMap(BaseModel):
|
|
37
|
+
"""A model for the route map."""
|
|
38
|
+
|
|
39
|
+
methods: List[str] | None = None
|
|
40
|
+
tags: List[str] | None = None
|
|
41
|
+
pattern: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ToolConfig(BaseModel):
|
|
45
|
+
"""A model for the tool configuration."""
|
|
46
|
+
|
|
47
|
+
api_maps: List[APIMap] | None = None
|
|
48
|
+
route_maps: List[RouteMap] | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AuthConfig(BaseModel):
|
|
52
|
+
"""A model for the authentication configuration."""
|
|
53
|
+
|
|
54
|
+
type: AuthenticationType = AuthenticationType.NONE
|
|
55
|
+
username: str | None = None
|
|
56
|
+
password: str | None = None
|
|
57
|
+
api_key_name: str | None = None
|
|
58
|
+
api_key_value: str | None = None
|
|
59
|
+
bearer_token: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class APIServerConfig(BaseModel):
|
|
63
|
+
"""A model for the API server configuration."""
|
|
64
|
+
|
|
65
|
+
server_name: str
|
|
66
|
+
endpoint: str
|
|
67
|
+
base_path: str | None = None
|
|
68
|
+
timeout: float = 10
|
|
69
|
+
tls_verify: bool = True
|
|
70
|
+
spec_file_path: str | Path
|
|
71
|
+
# NOTE: open_api_spec is not needed any more
|
|
72
|
+
# open_api_spec: AxmpOpenAPI | None = None
|
|
73
|
+
tool_config: ToolConfig
|
|
74
|
+
auth_config: AuthConfig
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MultiOpenAPISpecConfig(BaseModel):
|
|
78
|
+
"""MultiOpenAPISpecConfig is a model that contains the configuration for the multi-server API specification.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"backends": [
|
|
83
|
+
{
|
|
84
|
+
"server_name": "zcp-alert-backend",
|
|
85
|
+
"endpoint": "https://zcp-alert-backend.com",
|
|
86
|
+
"base_path": "/api/alert/v1",
|
|
87
|
+
"tls_verify": false,
|
|
88
|
+
"timeout": 10,
|
|
89
|
+
"auth_config": {
|
|
90
|
+
"type": "basic",
|
|
91
|
+
"username": "admin",
|
|
92
|
+
"password": "password"
|
|
93
|
+
},
|
|
94
|
+
"spec_file_path": "openapi_spec/zcp_spec/alert_openapi_spec.json",
|
|
95
|
+
"tool_config": {
|
|
96
|
+
"api_maps": [
|
|
97
|
+
{
|
|
98
|
+
"path":"/api/alert/v1/alerts",
|
|
99
|
+
"methods": ["get", "post"]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"path":"/api/alert/v1/alerts/webhook",
|
|
103
|
+
"methods": ["post"]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"path":"/api/alert/v1/alert/priorities",
|
|
107
|
+
"methods": ["get"]
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"server_name": "example-backend",
|
|
114
|
+
"endpoint": "https://example-backend.com",
|
|
115
|
+
"base_path": "/api/example/v1",
|
|
116
|
+
"spec_file_path": "openapi_spec/example_spec/test_spec.json",
|
|
117
|
+
"tls_verify": true,
|
|
118
|
+
"timeout": 10,
|
|
119
|
+
"auth_config": {
|
|
120
|
+
"type": "api_key",
|
|
121
|
+
"custom_header_api_key_name": "X-API-KEY",
|
|
122
|
+
"custom_header_api_key_value": "1234567890"
|
|
123
|
+
},
|
|
124
|
+
"tool_config": {
|
|
125
|
+
"api_maps": [
|
|
126
|
+
{
|
|
127
|
+
"path":"/api/example/v1/test",
|
|
128
|
+
"methods": [
|
|
129
|
+
{
|
|
130
|
+
"method": "post",
|
|
131
|
+
"tool_name": "create_test",
|
|
132
|
+
"description": "Create test"
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"route_maps": [
|
|
138
|
+
{
|
|
139
|
+
"pattern": r".*",
|
|
140
|
+
"methods": ["get"],
|
|
141
|
+
"tags": ["*"]
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"server_name": "zcp-alert-backend",
|
|
148
|
+
"endpoint": "https://zcp-alert-backend.com",
|
|
149
|
+
"base_path": "/api/alert/v1",
|
|
150
|
+
"spec_file_path": "openapi_spec/zcp_spec/alert_openapi_spec.json",
|
|
151
|
+
"tls_verify": true,
|
|
152
|
+
"timeout": 10,
|
|
153
|
+
"auth_config": {
|
|
154
|
+
"type": "bearer",
|
|
155
|
+
"bearer_token": "1234567890"
|
|
156
|
+
},
|
|
157
|
+
"tool_config": {
|
|
158
|
+
"route_maps": [
|
|
159
|
+
{
|
|
160
|
+
"pattern": "^/api/alert/v1/channels/.*",
|
|
161
|
+
"methods": ["get"],
|
|
162
|
+
"tags": []
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"pattern": "^/api/alert/v1/admin/.*",
|
|
166
|
+
"methods": ["post"],
|
|
167
|
+
"tags": ["alerts"]
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
backends: List[APIServerConfig]
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def from_multi_server_spec_file(
|
|
181
|
+
cls, file_path: str | Path
|
|
182
|
+
) -> MultiOpenAPISpecConfig:
|
|
183
|
+
"""Load the multi-server API specification from a file."""
|
|
184
|
+
with open(file_path) as f:
|
|
185
|
+
return cls.model_validate_json(f.read())
|