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.
Files changed (15) hide show
  1. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/PKG-INFO +3 -6
  2. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/README.md +2 -5
  3. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/server_fastmcp.py +55 -20
  4. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/server_lowlevel.py +21 -26
  5. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/utils.py +25 -103
  6. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/PKG-INFO +3 -6
  7. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/pyproject.toml +1 -1
  8. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/LICENSE +0 -0
  9. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy/__init__.py +0 -0
  10. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/SOURCES.txt +0 -0
  11. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/dependency_links.txt +0 -0
  12. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/entry_points.txt +0 -0
  13. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/requires.txt +0 -0
  14. {mcp_openapi_proxy-0.1.1741476231 → mcp_openapi_proxy-0.1.1741477332}/mcp_openapi_proxy.egg-info/top_level.txt +0 -0
  15. {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.1741476231
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., `query.token` for Slack).
111
- - `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for Fly.io).
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
- ![image](https://github.com/user-attachments/assets/6ae7f708-9494-41a1-9075-e685f2cd8873)
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., `query.token` for Slack).
95
- - `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for Fly.io).
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
- ![image](https://github.com/user-attachments/assets/6ae7f708-9494-41a1-9075-e685f2cd8873)
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() # Grab the prefix here
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}" # Apply prefix here
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() # Ensure prefix is considered here too
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
- # Apply custom auth mapping if API_KEY_JMESPATH is set
142
- parameters = handle_custom_auth(function_def["operation"], parameters)
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
- path = "/" + path.lstrip("/")
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
- if parameters is None:
165
- parameters = {}
166
- logger.debug(f"Parameters after auth handling: {parameters}")
167
- if parameters:
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
- logger.debug(f"Set request_body: {request_body}")
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=None if function_def["method"] == "GET" else request_body
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
- # Handle all path parameters, not just required ones
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:
@@ -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
- # Structure to apply JMESPath: separate query params and body
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
- if operation.get("method", "GET").upper() == "GET":
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
- else:
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
- # Compile JMESPath expression and set the API key, overwriting existing
259
- expr = jmespath.compile(jmespath_expr)
260
- updated_data = expr.search(request_data, options=jmespath.Options(dict_cls=dict))
261
- if updated_data is None:
262
- # If path doesn't exist, create it
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 # Force overwrite
217
+ current[part] = api_key # Overwrite at final key
278
218
  else:
279
219
  current = current.setdefault(part, {})
280
- logger.debug(f"Applied API_KEY to {jmespath_expr}, overwriting any existing: {redact_api_key(api_key)}")
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"Failed to apply API_KEY_JMESPATH '{jmespath_expr}': {e}")
283
- return parameters
224
+ logger.error(f"Error applying JMESPath expression {jmespath_expr}: {e}")
284
225
 
285
- # Flatten back to parameters, overwriting original params
286
- if operation.get("method", "GET").upper() == "GET":
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.1741476231
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., `query.token` for Slack).
111
- - `API_AUTH_TYPE`: (Optional) Overrides the default `Bearer` Authorization header type (e.g., `Api-Key` for Fly.io).
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
- ![image](https://github.com/user-attachments/assets/6ae7f708-9494-41a1-9075-e685f2cd8873)
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mcp-openapi-proxy"
7
- version = "0.1.1741476231"
7
+ version = "0.1.1741477332"
8
8
  description = "MCP server for exposing OpenAPI specifications as MCP tools."
9
9
  readme = "README.md"
10
10
  authors = [