mcp-openapi-proxy 0.1.1741476231__tar.gz → 0.1.1741477332__tar.gz
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.
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/PKG-INFO +3 -6
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/README.md +2 -5
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/server_fastmcp.py +55 -20
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/server_lowlevel.py +21 -26
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/utils.py +25 -103
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/PKG-INFO +3 -6
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/pyproject.toml +1 -1
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/LICENSE +0 -0
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/__init__.py +0 -0
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/SOURCES.txt +0 -0
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/dependency_links.txt +0 -0
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/entry_points.txt +0 -0
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/requires.txt +0 -0
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/top_level.txt +0 -0
- {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: mcp-openapi-proxy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1741477332
|
|
4
4
|
Summary: MCP server for exposing OpenAPI specifications as MCP tools.
|
|
5
5
|
Author-email: Matthew Hand <matthewhandau@gmail.com>
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -107,8 +107,8 @@ Refer to the **Examples** section below for practical configurations tailored to
|
|
|
107
107
|
- `TOOL_WHITELIST`: (Optional) A comma-separated list of endpoint paths to expose as tools.
|
|
108
108
|
- `TOOL_NAME_PREFIX`: (Optional) A prefix to prepend to all tool names.
|
|
109
109
|
- `API_KEY`: (Optional) Authentication token for the API, sent as `Bearer <API_KEY>` in the Authorization header by default.
|
|
110
|
-
- `API_KEY_JMESPATH`: (Optional) JMESPath expression to map `API_KEY` into request parameters (e.g., `
|
|
111
|
-
- `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for
|
|
110
|
+
- `API_KEY_JMESPATH`: (Optional) JMESPath expression to map `API_KEY` into request parameters (e.g., `token` for Slack).
|
|
111
|
+
- `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for GetZep).
|
|
112
112
|
|
|
113
113
|
## Examples
|
|
114
114
|
|
|
@@ -172,8 +172,6 @@ OPENAPI_SPEC_URL="https://raw.githubusercontent.com/abhiaagarwal/peristera/refs/
|
|
|
172
172
|
|
|
173
173
|
### Slack Example
|
|
174
174
|
|
|
175
|
-

|
|
176
|
-
|
|
177
175
|
Slack’s API showcases payload-based authentication with JMESPath. Obtain a bot token from [Slack API documentation](https://api.slack.com/authentication/token-types#bot).
|
|
178
176
|
|
|
179
177
|
#### 1. Verify the OpenAPI Specification
|
|
@@ -276,7 +274,6 @@ Update your configuration:
|
|
|
276
274
|
- **TOOL_WHITELIST**: Limits to `/sessions` endpoints.
|
|
277
275
|
- **API_KEY**: Your GetZep API key.
|
|
278
276
|
- **API_AUTH_TYPE**: Uses `Api-Key` for header-based authentication (overrides default `Bearer`).
|
|
279
|
-
- **SERVER_URL_OVERRIDE**: GetZep’s API base URL.
|
|
280
277
|
- **TOOL_NAME_PREFIX**: Prepends `getzep_` to tools.
|
|
281
278
|
|
|
282
279
|
#### 3. Resulting Tools
|
|
@@ -91,8 +91,8 @@ Refer to the **Examples** section below for practical configurations tailored to
|
|
|
91
91
|
- `TOOL_WHITELIST`: (Optional) A comma-separated list of endpoint paths to expose as tools.
|
|
92
92
|
- `TOOL_NAME_PREFIX`: (Optional) A prefix to prepend to all tool names.
|
|
93
93
|
- `API_KEY`: (Optional) Authentication token for the API, sent as `Bearer <API_KEY>` in the Authorization header by default.
|
|
94
|
-
- `API_KEY_JMESPATH`: (Optional) JMESPath expression to map `API_KEY` into request parameters (e.g., `
|
|
95
|
-
- `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for
|
|
94
|
+
- `API_KEY_JMESPATH`: (Optional) JMESPath expression to map `API_KEY` into request parameters (e.g., `token` for Slack).
|
|
95
|
+
- `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for GetZep).
|
|
96
96
|
|
|
97
97
|
## Examples
|
|
98
98
|
|
|
@@ -156,8 +156,6 @@ OPENAPI_SPEC_URL="https://raw.githubusercontent.com/abhiaagarwal/peristera/refs/
|
|
|
156
156
|
|
|
157
157
|
### Slack Example
|
|
158
158
|
|
|
159
|
-

|
|
160
|
-
|
|
161
159
|
Slack’s API showcases payload-based authentication with JMESPath. Obtain a bot token from [Slack API documentation](https://api.slack.com/authentication/token-types#bot).
|
|
162
160
|
|
|
163
161
|
#### 1. Verify the OpenAPI Specification
|
|
@@ -260,7 +258,6 @@ Update your configuration:
|
|
|
260
258
|
- **TOOL_WHITELIST**: Limits to `/sessions` endpoints.
|
|
261
259
|
- **API_KEY**: Your GetZep API key.
|
|
262
260
|
- **API_AUTH_TYPE**: Uses `Api-Key` for header-based authentication (overrides default `Bearer`).
|
|
263
|
-
- **SERVER_URL_OVERRIDE**: GetZep’s API base URL.
|
|
264
261
|
- **TOOL_NAME_PREFIX**: Prepends `getzep_` to tools.
|
|
265
262
|
|
|
266
263
|
#### 3. Resulting Tools
|
|
@@ -16,7 +16,8 @@ import os
|
|
|
16
16
|
import sys
|
|
17
17
|
import json
|
|
18
18
|
import requests
|
|
19
|
-
|
|
19
|
+
from typing import Dict, Any
|
|
20
|
+
from mcp import types
|
|
20
21
|
from mcp.server.fastmcp import FastMCP
|
|
21
22
|
from mcp_openapi_proxy.utils import setup_logging, is_tool_whitelisted, fetch_openapi_spec, get_auth_headers, build_base_url, normalize_tool_name, handle_custom_auth, get_tool_prefix
|
|
22
23
|
|
|
@@ -49,7 +50,7 @@ def list_functions(*, env_key: str = "OPENAPI_SPEC_URL") -> str:
|
|
|
49
50
|
logger.debug("No paths found in spec.")
|
|
50
51
|
return json.dumps([])
|
|
51
52
|
functions = {}
|
|
52
|
-
prefix = get_tool_prefix()
|
|
53
|
+
prefix = get_tool_prefix()
|
|
53
54
|
for path, path_item in paths.items():
|
|
54
55
|
logger.debug(f"Processing path: {path}")
|
|
55
56
|
if not path_item:
|
|
@@ -71,19 +72,37 @@ def list_functions(*, env_key: str = "OPENAPI_SPEC_URL") -> str:
|
|
|
71
72
|
raw_name = f"{method.upper()} {path}"
|
|
72
73
|
function_name = normalize_tool_name(raw_name)
|
|
73
74
|
if prefix:
|
|
74
|
-
function_name = f"{prefix}{function_name}"
|
|
75
|
+
function_name = f"{prefix}{function_name}"
|
|
75
76
|
if function_name in functions:
|
|
76
77
|
logger.debug(f"Skipping duplicate tool name: {function_name}")
|
|
77
78
|
continue
|
|
78
79
|
function_description = operation.get("summary", operation.get("description", "No description provided."))
|
|
79
80
|
logger.debug(f"Registering function: {function_name} - {function_description}")
|
|
81
|
+
input_schema = {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {},
|
|
84
|
+
"required": [],
|
|
85
|
+
"additionalProperties": False
|
|
86
|
+
}
|
|
87
|
+
for param in operation.get("parameters", []):
|
|
88
|
+
param_name = param.get("name")
|
|
89
|
+
param_type = param.get("type", "string")
|
|
90
|
+
if param_type not in ["string", "integer", "boolean", "number"]:
|
|
91
|
+
param_type = "string"
|
|
92
|
+
input_schema["properties"][param_name] = {
|
|
93
|
+
"type": param_type,
|
|
94
|
+
"description": param.get("description", f"{param.get('in', 'unknown')} parameter {param_name}")
|
|
95
|
+
}
|
|
96
|
+
if param.get("required", False):
|
|
97
|
+
input_schema["required"].append(param_name)
|
|
80
98
|
functions[function_name] = {
|
|
81
99
|
"name": function_name,
|
|
82
100
|
"description": function_description,
|
|
83
101
|
"path": path,
|
|
84
102
|
"method": method.upper(),
|
|
85
103
|
"operationId": operation.get("operationId"),
|
|
86
|
-
"original_name": raw_name
|
|
104
|
+
"original_name": raw_name,
|
|
105
|
+
"inputSchema": input_schema
|
|
87
106
|
}
|
|
88
107
|
logger.info(f"Discovered {len(functions)} functions from the OpenAPI specification.")
|
|
89
108
|
logger.debug(f"Functions list: {list(functions.values())}")
|
|
@@ -110,7 +129,7 @@ def call_function(*, function_name: str, parameters: dict = None, env_key: str =
|
|
|
110
129
|
function_def = None
|
|
111
130
|
paths = spec.get("paths", {})
|
|
112
131
|
logger.debug(f"Paths for function lookup: {list(paths.keys())}")
|
|
113
|
-
prefix = get_tool_prefix()
|
|
132
|
+
prefix = get_tool_prefix()
|
|
114
133
|
for path, path_item in paths.items():
|
|
115
134
|
logger.debug(f"Checking path: {path}")
|
|
116
135
|
for method, operation in path_item.items():
|
|
@@ -138,8 +157,10 @@ def call_function(*, function_name: str, parameters: dict = None, env_key: str =
|
|
|
138
157
|
return json.dumps({"error": f"Function '{function_name}' not found"})
|
|
139
158
|
logger.debug(f"Function def found: {function_def}")
|
|
140
159
|
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
operation = function_def["operation"]
|
|
161
|
+
operation["method"] = function_def["method"]
|
|
162
|
+
parameters = handle_custom_auth(operation, parameters)
|
|
163
|
+
logger.debug(f"Parameters after auth handling: {parameters}")
|
|
143
164
|
|
|
144
165
|
if not is_tool_whitelisted(function_def["path"]):
|
|
145
166
|
logger.error(f"Access to function '{function_name}' is not allowed.")
|
|
@@ -151,26 +172,40 @@ def call_function(*, function_name: str, parameters: dict = None, env_key: str =
|
|
|
151
172
|
return json.dumps({"error": "No base URL defined in spec or SERVER_URL_OVERRIDE"})
|
|
152
173
|
|
|
153
174
|
path = function_def["path"]
|
|
154
|
-
|
|
155
|
-
logger.debug(f"Normalized path: {path}")
|
|
156
|
-
api_url = base_url + path
|
|
157
|
-
logger.debug(f"Final API URL: {api_url}")
|
|
158
|
-
request_params = {}
|
|
159
|
-
request_body = None
|
|
175
|
+
api_url = f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
160
176
|
headers = {}
|
|
161
177
|
if function_def["method"] != "GET":
|
|
162
178
|
headers["Content-Type"] = "application/json"
|
|
163
179
|
headers.update(get_auth_headers(spec))
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
request_params = {}
|
|
181
|
+
request_body = None
|
|
182
|
+
|
|
183
|
+
# Map all path parameters dynamically from the spec
|
|
184
|
+
if isinstance(parameters, dict):
|
|
185
|
+
path_params_in_openapi = [
|
|
186
|
+
param["name"] for param in operation.get("parameters", []) if param.get("in") == "path"
|
|
187
|
+
]
|
|
188
|
+
if path_params_in_openapi:
|
|
189
|
+
missing_required = [
|
|
190
|
+
param["name"] for param in operation.get("parameters", [])
|
|
191
|
+
if param.get("in") == "path" and param.get("required", False) and param["name"] not in parameters
|
|
192
|
+
]
|
|
193
|
+
if missing_required:
|
|
194
|
+
logger.error(f"Missing required path parameters: {missing_required}")
|
|
195
|
+
return json.dumps({"error": f"Missing required path parameters: {missing_required}"})
|
|
196
|
+
for param_name in path_params_in_openapi:
|
|
197
|
+
if param_name in parameters:
|
|
198
|
+
api_url = api_url.replace(f"{{{param_name}}}", str(parameters.pop(param_name)))
|
|
199
|
+
logger.debug(f"Replaced path param {param_name} in URL: {api_url}")
|
|
200
|
+
# Remaining parameters go to query (GET) or body (non-GET)
|
|
168
201
|
if function_def["method"] == "GET":
|
|
169
202
|
request_params = parameters
|
|
170
|
-
logger.debug(f"Set request_params for GET: {request_params}")
|
|
171
203
|
else:
|
|
172
204
|
request_body = parameters
|
|
173
|
-
|
|
205
|
+
else:
|
|
206
|
+
parameters = {}
|
|
207
|
+
logger.debug("No valid parameters provided, proceeding without params/body")
|
|
208
|
+
|
|
174
209
|
logger.debug(f"Sending request - Method: {function_def['method']}, URL: {api_url}, Headers: {headers}, Params: {request_params}, Body: {request_body}")
|
|
175
210
|
try:
|
|
176
211
|
response = requests.request(
|
|
@@ -178,7 +213,7 @@ def call_function(*, function_name: str, parameters: dict = None, env_key: str =
|
|
|
178
213
|
url=api_url,
|
|
179
214
|
headers=headers,
|
|
180
215
|
params=request_params if function_def["method"] == "GET" else None,
|
|
181
|
-
json=
|
|
216
|
+
json=request_body if function_def["method"] != "GET" else None
|
|
182
217
|
)
|
|
183
218
|
response.raise_for_status()
|
|
184
219
|
logger.debug(f"API response received: {response.text}")
|
|
@@ -78,33 +78,28 @@ async def dispatcher_handler(request: types.CallToolRequest) -> types.ServerResu
|
|
|
78
78
|
request_params = {}
|
|
79
79
|
request_body = None
|
|
80
80
|
|
|
81
|
-
#
|
|
82
|
-
path_params_in_openapi = [
|
|
83
|
-
param['name'] for param in operation.get('parameters', []) if param.get('in') == 'path'
|
|
84
|
-
]
|
|
85
|
-
if path_params_in_openapi:
|
|
86
|
-
if not isinstance(parameters, dict):
|
|
87
|
-
logger.error(f"Path parameters {path_params_in_openapi} missing: parameters is not a dict")
|
|
88
|
-
return types.ServerResult(
|
|
89
|
-
root=types.CallToolResult(
|
|
90
|
-
content=[types.TextContent(type="text", text=f"Missing path parameters: {path_params_in_openapi}")]
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
missing_params = [p for p in path_params_in_openapi if p not in parameters]
|
|
94
|
-
if missing_params:
|
|
95
|
-
logger.error(f"Required path parameters missing: {missing_params}")
|
|
96
|
-
return types.ServerResult(
|
|
97
|
-
root=types.CallToolResult(
|
|
98
|
-
content=[types.TextContent(type="text", text=f"Missing required path parameters: {missing_params}")]
|
|
99
|
-
)
|
|
100
|
-
)
|
|
101
|
-
for param_name in path_params_in_openapi:
|
|
102
|
-
if param_name in parameters:
|
|
103
|
-
api_url = api_url.replace(f"{{{param_name}}}", str(parameters.pop(param_name)))
|
|
104
|
-
logger.debug(f"Replaced path param {param_name} in URL: {api_url}")
|
|
105
|
-
|
|
106
|
-
# Remaining parameters go to query (GET) or body (non-GET)
|
|
81
|
+
# Map all path parameters dynamically from the spec
|
|
107
82
|
if isinstance(parameters, dict):
|
|
83
|
+
path_params_in_openapi = [
|
|
84
|
+
param['name'] for param in operation.get('parameters', []) if param.get('in') == 'path'
|
|
85
|
+
]
|
|
86
|
+
if path_params_in_openapi:
|
|
87
|
+
missing_required = [
|
|
88
|
+
param['name'] for param in operation.get('parameters', [])
|
|
89
|
+
if param.get('in') == 'path' and param.get('required', False) and param['name'] not in parameters
|
|
90
|
+
]
|
|
91
|
+
if missing_required:
|
|
92
|
+
logger.error(f"Missing required path parameters: {missing_required}")
|
|
93
|
+
return types.ServerResult(
|
|
94
|
+
root=types.CallToolResult(
|
|
95
|
+
content=[types.TextContent(type="text", text=f"Missing required path parameters: {missing_required}")]
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
for param_name in path_params_in_openapi:
|
|
99
|
+
if param_name in parameters:
|
|
100
|
+
api_url = api_url.replace(f"{{{param_name}}}", str(parameters.pop(param_name)))
|
|
101
|
+
logger.debug(f"Replaced path param {param_name} in URL: {api_url}")
|
|
102
|
+
# Remaining parameters go to query (GET) or body (non-GET)
|
|
108
103
|
if method == "GET":
|
|
109
104
|
request_params = parameters
|
|
110
105
|
else:
|
{mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/utils.py
RENAMED
|
@@ -21,15 +21,6 @@ load_dotenv()
|
|
|
21
21
|
OPENAPI_SPEC_URL = os.getenv("OPENAPI_SPEC_URL")
|
|
22
22
|
|
|
23
23
|
def setup_logging(debug: bool = False) -> logging.Logger:
|
|
24
|
-
"""
|
|
25
|
-
Configures logging for the application, directing all output to stderr.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
debug (bool): If True, sets log level to DEBUG; otherwise, INFO.
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
logging.Logger: Configured logger instance.
|
|
32
|
-
"""
|
|
33
24
|
logger = logging.getLogger(__name__)
|
|
34
25
|
logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
35
26
|
logger.propagate = False
|
|
@@ -53,21 +44,11 @@ logger.debug(f"OpenAPI Spec URL: {OPENAPI_SPEC_URL}")
|
|
|
53
44
|
logger.debug("utils.py initialized")
|
|
54
45
|
|
|
55
46
|
def redact_api_key(key: str) -> str:
|
|
56
|
-
"""Redacts an API key for secure logging."""
|
|
57
47
|
if not key or len(key) <= 4:
|
|
58
48
|
return "<not set>"
|
|
59
49
|
return f"{key[:2]}{'*' * (len(key) - 4)}{key[-2:]}"
|
|
60
50
|
|
|
61
51
|
def normalize_tool_name(name: str) -> str:
|
|
62
|
-
"""
|
|
63
|
-
Normalizes tool names into a clean function name without parameters.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
name (str): Raw method and path, e.g., 'GET /sessions/{sessionId}/messages/{messageUUID}'.
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
str: Normalized tool name without special characters.
|
|
70
|
-
"""
|
|
71
52
|
if not name or not isinstance(name, str):
|
|
72
53
|
logger.warning(f"Invalid tool name input: {name}. Defaulting to 'unknown_tool'.")
|
|
73
54
|
return "unknown_tool"
|
|
@@ -99,22 +80,12 @@ def normalize_tool_name(name: str) -> str:
|
|
|
99
80
|
return func_name or "unknown_tool"
|
|
100
81
|
|
|
101
82
|
def get_tool_prefix() -> str:
|
|
102
|
-
"""Retrieves tool name prefix from environment, ensuring it ends with an underscore."""
|
|
103
83
|
prefix = os.getenv("TOOL_NAME_PREFIX", "")
|
|
104
84
|
if prefix and not prefix.endswith("_"):
|
|
105
85
|
prefix += "_"
|
|
106
86
|
return prefix
|
|
107
87
|
|
|
108
88
|
def is_tool_whitelisted(endpoint: str) -> bool:
|
|
109
|
-
"""
|
|
110
|
-
Checks if an endpoint matches any partial path in TOOL_WHITELIST.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
endpoint (str): The endpoint path from the OpenAPI spec.
|
|
114
|
-
|
|
115
|
-
Returns:
|
|
116
|
-
bool: True if the endpoint matches any whitelist item, False otherwise.
|
|
117
|
-
"""
|
|
118
89
|
whitelist = os.getenv("TOOL_WHITELIST", "")
|
|
119
90
|
logger.debug(f"Checking whitelist - endpoint: {endpoint}, TOOL_WHITELIST: {whitelist}")
|
|
120
91
|
if not whitelist:
|
|
@@ -152,7 +123,6 @@ def is_tool_whitelisted(endpoint: str) -> bool:
|
|
|
152
123
|
return False
|
|
153
124
|
|
|
154
125
|
def fetch_openapi_spec(spec_url: str) -> dict:
|
|
155
|
-
"""Fetches and parses OpenAPI specification from a URL or file, assuming JSON if no suffix."""
|
|
156
126
|
try:
|
|
157
127
|
if spec_url.startswith("file://"):
|
|
158
128
|
spec_path = spec_url.replace("file://", "")
|
|
@@ -160,12 +130,13 @@ def fetch_openapi_spec(spec_url: str) -> dict:
|
|
|
160
130
|
content = f.read()
|
|
161
131
|
logger.debug(f"Read local OpenAPI spec from {spec_path}")
|
|
162
132
|
else:
|
|
163
|
-
response = requests.get(spec_url)
|
|
133
|
+
response = requests.get(spec_url, timeout=10)
|
|
134
|
+
if response.status_code in [401, 403]:
|
|
135
|
+
logger.debug(f"Spec {spec_url} requires auth (status {response.status_code})—skipping")
|
|
136
|
+
return None
|
|
164
137
|
response.raise_for_status()
|
|
165
138
|
content = response.text
|
|
166
139
|
logger.debug(f"Fetched OpenAPI spec from {spec_url}")
|
|
167
|
-
|
|
168
|
-
# Check suffix, default to JSON if none
|
|
169
140
|
if spec_url.endswith(('.yaml', '.yml')):
|
|
170
141
|
spec = yaml.safe_load(content)
|
|
171
142
|
logger.debug(f"Parsed YAML OpenAPI spec from {spec_url}")
|
|
@@ -181,16 +152,6 @@ def fetch_openapi_spec(spec_url: str) -> dict:
|
|
|
181
152
|
return None
|
|
182
153
|
|
|
183
154
|
def get_auth_headers(spec: dict, api_key_env: str = "API_KEY") -> dict:
|
|
184
|
-
"""
|
|
185
|
-
Constructs authorization headers based on spec and environment variables.
|
|
186
|
-
|
|
187
|
-
Args:
|
|
188
|
-
spec (dict): OpenAPI specification.
|
|
189
|
-
api_key_env (str): Environment variable name for the API key (default: "API_KEY").
|
|
190
|
-
|
|
191
|
-
Returns:
|
|
192
|
-
dict: Headers dictionary with Authorization set appropriately.
|
|
193
|
-
"""
|
|
194
155
|
headers = {}
|
|
195
156
|
auth_token = os.getenv(api_key_env)
|
|
196
157
|
if not auth_token:
|
|
@@ -223,75 +184,54 @@ def get_auth_headers(spec: dict, api_key_env: str = "API_KEY") -> dict:
|
|
|
223
184
|
return headers
|
|
224
185
|
|
|
225
186
|
def handle_custom_auth(operation: dict, parameters: dict = None) -> dict:
|
|
226
|
-
"""
|
|
227
|
-
Applies custom authentication mapping using API_KEY_JMESPATH if provided, overwriting existing keys.
|
|
228
|
-
|
|
229
|
-
Args:
|
|
230
|
-
operation (dict): The OpenAPI operation object for the endpoint.
|
|
231
|
-
parameters (dict, optional): Existing parameters or arguments to modify.
|
|
232
|
-
|
|
233
|
-
Returns:
|
|
234
|
-
dict: Updated parameters with API_KEY mapped according to API_KEY_JMESPATH, overwriting conflicts.
|
|
235
|
-
"""
|
|
236
187
|
if parameters is None:
|
|
237
188
|
parameters = {}
|
|
238
|
-
|
|
239
189
|
logger.debug(f"Raw parameters before auth handling: {parameters}")
|
|
240
190
|
api_key = os.getenv("API_KEY")
|
|
241
191
|
jmespath_expr = os.getenv("API_KEY_JMESPATH")
|
|
242
|
-
|
|
243
192
|
if not api_key or not jmespath_expr:
|
|
244
193
|
logger.debug("No API_KEY or API_KEY_JMESPATH set, skipping custom auth handling.")
|
|
245
194
|
return parameters
|
|
246
195
|
|
|
247
|
-
|
|
196
|
+
method = operation.get("method", "GET").upper()
|
|
248
197
|
request_data = {"query": {}, "body": {}}
|
|
198
|
+
# Preserve original params, split by method
|
|
249
199
|
if parameters:
|
|
250
|
-
# Assume GET params go to query, others to body (simplified heuristic)
|
|
251
200
|
for key, value in parameters.items():
|
|
252
|
-
|
|
201
|
+
param_in = next((p.get("in") for p in operation.get("parameters", []) if p.get("name") == key), None)
|
|
202
|
+
if param_in == "query" or (method == "GET" and param_in not in ["path", "header"]):
|
|
253
203
|
request_data["query"][key] = value
|
|
254
|
-
|
|
204
|
+
elif param_in == "header":
|
|
205
|
+
request_data["body"][key] = value
|
|
206
|
+
elif param_in != "path": # Path params handled elsewhere
|
|
255
207
|
request_data["body"][key] = value
|
|
256
208
|
|
|
209
|
+
# Apply API_KEY via JMESPath
|
|
257
210
|
try:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
parts = jmespath_expr.split('.')
|
|
264
|
-
current = request_data
|
|
265
|
-
for i, part in enumerate(parts):
|
|
266
|
-
if i == len(parts) - 1:
|
|
267
|
-
current[part] = api_key # Overwrite here
|
|
268
|
-
else:
|
|
269
|
-
current.setdefault(part, {})
|
|
270
|
-
current = current[part]
|
|
271
|
-
else:
|
|
272
|
-
# Overwrite existing value at the JMESPath location
|
|
273
|
-
parts = jmespath_expr.split('.')
|
|
274
|
-
current = request_data
|
|
275
|
-
for i, part in enumerate(parts):
|
|
211
|
+
if jmespath_expr:
|
|
212
|
+
parts = jmespath_expr.split(".")
|
|
213
|
+
target = request_data[parts[0]] if parts[0] in request_data else {}
|
|
214
|
+
current = target
|
|
215
|
+
for i, part in enumerate(parts[1:], 1):
|
|
276
216
|
if i == len(parts) - 1:
|
|
277
|
-
current[part] = api_key #
|
|
217
|
+
current[part] = api_key # Overwrite at final key
|
|
278
218
|
else:
|
|
279
219
|
current = current.setdefault(part, {})
|
|
280
|
-
|
|
220
|
+
if parts[0] in request_data:
|
|
221
|
+
request_data[parts[0]] = target
|
|
222
|
+
logger.debug(f"Applied API_KEY to {jmespath_expr}: {redact_api_key(api_key)}")
|
|
281
223
|
except Exception as e:
|
|
282
|
-
logger.error(f"
|
|
283
|
-
return parameters
|
|
224
|
+
logger.error(f"Error applying JMESPath expression {jmespath_expr}: {e}")
|
|
284
225
|
|
|
285
|
-
#
|
|
286
|
-
if
|
|
226
|
+
# Merge back, overwriting original params
|
|
227
|
+
if method == "GET":
|
|
287
228
|
parameters = {**parameters, **request_data["query"]}
|
|
288
229
|
else:
|
|
289
230
|
parameters = {**parameters, **request_data["body"]}
|
|
290
|
-
|
|
231
|
+
logger.debug(f"Parameters after custom auth merge: {parameters}")
|
|
291
232
|
return parameters
|
|
292
233
|
|
|
293
234
|
def map_schema_to_tools(schema: dict) -> list:
|
|
294
|
-
"""Maps a schema to a list of MCP tools."""
|
|
295
235
|
tools = []
|
|
296
236
|
classes = schema.get("classes", [])
|
|
297
237
|
for entry in classes:
|
|
@@ -308,15 +248,6 @@ def map_schema_to_tools(schema: dict) -> list:
|
|
|
308
248
|
return tools
|
|
309
249
|
|
|
310
250
|
def detect_response_type(response_text: str) -> tuple[types.TextContent, str]:
|
|
311
|
-
"""
|
|
312
|
-
Detects the response type (JSON or text) and returns the appropriate MCP content object.
|
|
313
|
-
|
|
314
|
-
Args:
|
|
315
|
-
response_text (str): The raw response text from the HTTP request.
|
|
316
|
-
|
|
317
|
-
Returns:
|
|
318
|
-
Tuple: (content object, log message)
|
|
319
|
-
"""
|
|
320
251
|
try:
|
|
321
252
|
json_data = json.loads(response_text)
|
|
322
253
|
structured_text = {"text": response_text}
|
|
@@ -328,15 +259,6 @@ def detect_response_type(response_text: str) -> tuple[types.TextContent, str]:
|
|
|
328
259
|
return content, log_message
|
|
329
260
|
|
|
330
261
|
def build_base_url(spec: dict) -> str:
|
|
331
|
-
"""
|
|
332
|
-
Constructs the base URL for API requests, prioritizing SERVER_URL_OVERRIDE.
|
|
333
|
-
|
|
334
|
-
Args:
|
|
335
|
-
spec (dict): OpenAPI specification containing servers or host information.
|
|
336
|
-
|
|
337
|
-
Returns:
|
|
338
|
-
str: The constructed base URL, or empty string if not determinable.
|
|
339
|
-
"""
|
|
340
262
|
override = os.getenv("SERVER_URL_OVERRIDE", "").strip()
|
|
341
263
|
if override:
|
|
342
264
|
urls = override.split()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: mcp-openapi-proxy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1741477332
|
|
4
4
|
Summary: MCP server for exposing OpenAPI specifications as MCP tools.
|
|
5
5
|
Author-email: Matthew Hand <matthewhandau@gmail.com>
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -107,8 +107,8 @@ Refer to the **Examples** section below for practical configurations tailored to
|
|
|
107
107
|
- `TOOL_WHITELIST`: (Optional) A comma-separated list of endpoint paths to expose as tools.
|
|
108
108
|
- `TOOL_NAME_PREFIX`: (Optional) A prefix to prepend to all tool names.
|
|
109
109
|
- `API_KEY`: (Optional) Authentication token for the API, sent as `Bearer <API_KEY>` in the Authorization header by default.
|
|
110
|
-
- `API_KEY_JMESPATH`: (Optional) JMESPath expression to map `API_KEY` into request parameters (e.g., `
|
|
111
|
-
- `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for
|
|
110
|
+
- `API_KEY_JMESPATH`: (Optional) JMESPath expression to map `API_KEY` into request parameters (e.g., `token` for Slack).
|
|
111
|
+
- `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for GetZep).
|
|
112
112
|
|
|
113
113
|
## Examples
|
|
114
114
|
|
|
@@ -172,8 +172,6 @@ OPENAPI_SPEC_URL="https://raw.githubusercontent.com/abhiaagarwal/peristera/refs/
|
|
|
172
172
|
|
|
173
173
|
### Slack Example
|
|
174
174
|
|
|
175
|
-

|
|
176
|
-
|
|
177
175
|
Slack’s API showcases payload-based authentication with JMESPath. Obtain a bot token from [Slack API documentation](https://api.slack.com/authentication/token-types#bot).
|
|
178
176
|
|
|
179
177
|
#### 1. Verify the OpenAPI Specification
|
|
@@ -276,7 +274,6 @@ Update your configuration:
|
|
|
276
274
|
- **TOOL_WHITELIST**: Limits to `/sessions` endpoints.
|
|
277
275
|
- **API_KEY**: Your GetZep API key.
|
|
278
276
|
- **API_AUTH_TYPE**: Uses `Api-Key` for header-based authentication (overrides default `Bearer`).
|
|
279
|
-
- **SERVER_URL_OVERRIDE**: GetZep’s API base URL.
|
|
280
277
|
- **TOOL_NAME_PREFIX**: Prepends `getzep_` to tools.
|
|
281
278
|
|
|
282
279
|
#### 3. Resulting Tools
|
|
File without changes
|
{mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|