mcp-openapi-proxy 0.1.1741340797__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.
File without changes
@@ -0,0 +1,53 @@
1
+ """
2
+ Entry point for the mcp_openapi_proxy package.
3
+
4
+ This script determines which server to run based on the presence of
5
+ the OPENAPI_SIMPLE_MODE environment variable:
6
+ - Low-Level Server: For dynamic tool creation from OpenAPI spec.
7
+ - FastMCP Server: For static tool configurations defined in code.
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ from dotenv import load_dotenv
13
+ from mcp_openapi_proxy.utils import setup_logging
14
+
15
+ # Load environment variables from .env if present
16
+ load_dotenv()
17
+
18
+ def main():
19
+ """
20
+ Main entry point for the mcp_openapi_proxy package.
21
+
22
+ Depending on the OPENAPI_SIMPLE_MODE environment variable, this function
23
+ launches either:
24
+ - Low-Level Server (dynamic tools from OpenAPI spec)
25
+ - FastMCP Server (static tools defined in server_fastmcp.py)
26
+ """
27
+ # Configure logging
28
+ DEBUG = os.getenv("DEBUG", "").lower() in ("true", "1", "yes")
29
+ logger = setup_logging(debug=DEBUG)
30
+
31
+ logger.debug("Starting mcp_openapi_proxy package entry point.")
32
+
33
+ # Default to Low-Level Mode unless SIMPLE_MODE is explicitly enabled
34
+ OPENAPI_SIMPLE_MODE = os.getenv("OPENAPI_SIMPLE_MODE", "false").lower() in ("true", "1", "yes") # Default to false, enable with "true" etc.
35
+ if OPENAPI_SIMPLE_MODE:
36
+ logger.debug("OPENAPI_SIMPLE_MODE is enabled. Launching FastMCP Server.")
37
+ from mcp_openapi_proxy.server_fastmcp import run_simple_server
38
+ selected_server = run_simple_server
39
+ else:
40
+ logger.debug("OPENAPI_SIMPLE_MODE is disabled. Launching Low-Level Server.")
41
+ from mcp_openapi_proxy.server_lowlevel import run_server
42
+ selected_server = run_server
43
+
44
+ # Run the selected server
45
+ try:
46
+ selected_server()
47
+ except Exception as e:
48
+ logger.critical("Unhandled exception occurred while running the server.", exc_info=True)
49
+ sys.exit(1)
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
@@ -0,0 +1,250 @@
1
+ """
2
+ Provides the FastMCP server logic for mcp-openapi-proxy.
3
+
4
+ This server exposes a pre-defined set of functions based on an OpenAPI specification.
5
+ Configuration is controlled via environment variables:
6
+
7
+ - OPENAPI_SPEC_URL_<hash>: Unique URL per test, falls back to OPENAPI_SPEC_URL.
8
+ - TOOL_WHITELIST: Comma-separated list of allowed endpoint paths.
9
+ - SERVER_URL_OVERRIDE: (Optional) Overrides the base URL from the OpenAPI spec.
10
+ - API_AUTH_BEARER: (Optional) Token for endpoints requiring authentication.
11
+ - API_AUTH_TYPE_OVERRIDE: (Optional) 'Bearer' or 'Api-Key'.
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import json
17
+ import requests
18
+ import logging
19
+
20
+ logging.basicConfig(
21
+ level=logging.DEBUG,
22
+ format='%(asctime)s %(levelname)s %(message)s',
23
+ datefmt='%Y-%m-%d %H:%M:%S'
24
+ )
25
+ logger = logging.getLogger(__name__)
26
+
27
+ logger.debug(f"Server CWD: {os.getcwd()}")
28
+ logger.debug(f"Server sys.path: {sys.path}")
29
+
30
+ from mcp.server.fastmcp import FastMCP
31
+ from mcp_openapi_proxy.utils import setup_logging, is_tool_whitelisted, get_auth_type
32
+
33
+ mcp = FastMCP("OpenApiProxy-Fast")
34
+
35
+ def fetch_openapi_spec(spec_url: str) -> dict:
36
+ logger.debug(f"Starting fetch_openapi_spec with spec_url: {spec_url}")
37
+ if not spec_url:
38
+ logger.error("spec_url is empty or None")
39
+ return None
40
+ logger.debug(f"Current CWD in fetch_openapi_spec: {os.getcwd()}")
41
+ try:
42
+ if spec_url.startswith("file://"):
43
+ spec_path = os.path.abspath(spec_url.replace("file://", ""))
44
+ logger.debug(f"Spec path after file:// strip and abspath: {spec_path}")
45
+ if not os.path.exists(spec_path):
46
+ logger.error(f"File does not exist at: {spec_path}")
47
+ return None
48
+ logger.debug(f"File exists at: {spec_path}")
49
+ with open(spec_path, 'r') as f:
50
+ spec = json.load(f)
51
+ logger.debug(f"Successfully read local OpenAPI spec from {spec_path}")
52
+ else:
53
+ logger.debug(f"Fetching remote spec from {spec_url}")
54
+ response = requests.get(spec_url)
55
+ response.raise_for_status()
56
+ spec = json.loads(response.text)
57
+ logger.debug(f"Successfully fetched OpenAPI spec from {spec_url}")
58
+ if not spec:
59
+ logger.error(f"Spec is empty after loading from {spec_url}")
60
+ return None
61
+ logger.debug(f"Spec keys loaded: {list(spec.keys())}")
62
+ logger.debug(f"Spec paths: {list(spec.get('paths', {}).keys())}")
63
+ return spec
64
+ except Exception as e:
65
+ logger.error(f"Failed to fetch or parse spec from {spec_url}: {e}", exc_info=True)
66
+ return None
67
+
68
+ @mcp.tool()
69
+ def list_functions(*, env_key: str = "OPENAPI_SPEC_URL") -> str:
70
+ logger.debug("Executing list_functions tool.")
71
+ spec_url = os.environ.get(env_key, os.environ.get("OPENAPI_SPEC_URL"))
72
+ whitelist = os.getenv('TOOL_WHITELIST')
73
+ logger.debug(f"Using spec_url: {spec_url}")
74
+ logger.debug(f"TOOL_WHITELIST value: {whitelist}")
75
+ if not spec_url:
76
+ logger.error("No OPENAPI_SPEC_URL or custom env_key configured.")
77
+ return json.dumps([])
78
+ logger.debug(f"Calling fetch_openapi_spec with: {spec_url}")
79
+ spec = fetch_openapi_spec(spec_url)
80
+ if spec is None:
81
+ logger.error("Spec is None after fetch_openapi_spec")
82
+ return json.dumps([])
83
+ logger.debug(f"Raw spec loaded: {json.dumps(spec, indent=2)}")
84
+ logger.debug(f"Spec loaded with keys: {list(spec.keys())}")
85
+ paths = spec.get("paths", {})
86
+ logger.debug(f"Paths extracted from spec: {list(paths.keys())}")
87
+ if not paths:
88
+ logger.debug("No paths found in spec.")
89
+ return json.dumps([])
90
+ functions = []
91
+ for path, path_item in paths.items():
92
+ logger.debug(f"Processing path: {path}")
93
+ if not path_item:
94
+ logger.debug(f"Path item is empty for {path}")
95
+ continue
96
+ whitelist_check = is_tool_whitelisted(path)
97
+ logger.debug(f"Whitelist check for {path}: {whitelist_check}")
98
+ if not whitelist_check:
99
+ logger.debug(f"Path {path} not in whitelist - skipping.")
100
+ continue
101
+ for method, operation in path_item.items():
102
+ logger.debug(f"Found method: {method} for path: {path}")
103
+ if not method:
104
+ logger.debug(f"Method is empty for {path}")
105
+ continue
106
+ if method.lower() not in ["get", "post", "put", "delete", "patch"]:
107
+ logger.debug(f"Method {method} not supported for {path} - skipping.")
108
+ continue
109
+ function_name = f"{method.upper()} {path}"
110
+ function_description = operation.get("summary", operation.get("description", "No description provided."))
111
+ logger.debug(f"Registering function: {function_name} - {function_description}")
112
+ functions.append({
113
+ "name": function_name,
114
+ "description": function_description,
115
+ "path": path,
116
+ "method": method.upper(),
117
+ "operationId": operation.get("operationId")
118
+ })
119
+ logger.info(f"Discovered {len(functions)} functions from the OpenAPI specification.")
120
+ logger.debug(f"Functions list: {functions}")
121
+ return json.dumps(functions, indent=2)
122
+
123
+ @mcp.tool()
124
+ def call_function(*, function_name: str, parameters: dict = None, env_key: str = "OPENAPI_SPEC_URL") -> str:
125
+ logger.debug(f"call_function invoked with function_name='{function_name}' and parameters={parameters}")
126
+ if not function_name:
127
+ logger.error("function_name is empty or None")
128
+ return json.dumps({"error": "function_name is required"})
129
+ spec_url = os.environ.get(env_key, os.environ.get("OPENAPI_SPEC_URL"))
130
+ if not spec_url:
131
+ logger.error("No OPENAPI_SPEC_URL or custom env_key configured.")
132
+ return json.dumps({"error": "OPENAPI_SPEC_URL is not configured"})
133
+ logger.debug(f"Fetching spec for call_function: {spec_url}")
134
+ spec = fetch_openapi_spec(spec_url)
135
+ if spec is None:
136
+ logger.error("Spec is None for call_function")
137
+ return json.dumps({"error": "Failed to fetch or parse the OpenAPI specification"})
138
+ logger.debug(f"Spec keys for call_function: {list(spec.keys())}")
139
+ API_AUTH_TYPE = os.getenv("API_AUTH_TYPE_OVERRIDE", get_auth_type(spec))
140
+ logger.debug(f"API_AUTH_TYPE set to: {API_AUTH_TYPE}")
141
+ function_def = None
142
+ paths = spec.get("paths", {})
143
+ logger.debug(f"Paths for function lookup: {list(paths.keys())}")
144
+ for path, path_item in paths.items():
145
+ logger.debug(f"Checking path: {path}")
146
+ for method, operation in path_item.items():
147
+ logger.debug(f"Checking method: {method} for {path}")
148
+ if method.lower() not in ["get", "post", "put", "delete", "patch"]:
149
+ logger.debug(f"Skipping unsupported method: {method}")
150
+ continue
151
+ current_function_name = f"{method.upper()} {path}"
152
+ logger.debug(f"Comparing {current_function_name} with {function_name}")
153
+ if current_function_name == function_name:
154
+ function_def = {
155
+ "path": path,
156
+ "method": method.upper(),
157
+ "operation": operation
158
+ }
159
+ logger.debug(f"Matched function definition for '{function_name}': {function_def}")
160
+ break
161
+ if function_def:
162
+ break
163
+ if not function_def:
164
+ logger.error(f"Function '{function_name}' not found in the OpenAPI specification.")
165
+ return json.dumps({"error": f"Function '{function_name}' not found"})
166
+ logger.debug(f"Function def found: {function_def}")
167
+ if not is_tool_whitelisted(function_def["path"]):
168
+ logger.error(f"Access to function '{function_name}' is not allowed.")
169
+ return json.dumps({"error": f"Access to function '{function_name}' is not allowed"})
170
+ SERVER_URL_OVERRIDE = os.getenv("SERVER_URL_OVERRIDE")
171
+ if SERVER_URL_OVERRIDE:
172
+ base_url = SERVER_URL_OVERRIDE.strip()
173
+ logger.debug(f"Using SERVER_URL_OVERRIDE: {base_url}")
174
+ else:
175
+ servers = spec.get("servers", [])
176
+ logger.debug(f"Servers from spec: {servers}")
177
+ if servers:
178
+ base_url = servers[0].get("url", "")
179
+ logger.debug(f"Using base_url from OpenAPI 3.0 servers: {base_url}")
180
+ else:
181
+ schemes = spec.get("schemes", ["https"])
182
+ host = spec.get("host", "example.com")
183
+ base_url = f"{schemes[0]}://{host}"
184
+ logger.debug(f"Using Swagger 2.0 fallback base_url: {base_url}")
185
+ if not base_url:
186
+ logger.warning("No valid base URL found in spec or override; using empty string.")
187
+ base_url = ""
188
+ base_url = base_url.rstrip("/")
189
+ logger.debug(f"Normalized base_url: {base_url}")
190
+ base_path = spec.get("basePath", "")
191
+ if base_path:
192
+ base_path = "/" + base_path.strip("/")
193
+ logger.debug(f"Normalized base_path: {base_path}")
194
+ if base_path not in base_url:
195
+ base_url = base_url + base_path
196
+ logger.debug(f"Added base_path to URL: {base_url}")
197
+ path = function_def["path"]
198
+ path = "/" + path.lstrip("/")
199
+ logger.debug(f"Normalized path: {path}")
200
+ api_url = base_url + path
201
+ logger.debug(f"Final API URL: {api_url}")
202
+ request_params = {}
203
+ request_body = None
204
+ headers = {"Content-Type": "application/json"}
205
+ if parameters is None:
206
+ logger.debug("Parameters is None, using empty dict")
207
+ parameters = {}
208
+ logger.debug(f"Parameters received: {parameters}")
209
+ if parameters:
210
+ if function_def["method"] == "GET":
211
+ request_params = parameters
212
+ logger.debug(f"Set request_params for GET: {request_params}")
213
+ else:
214
+ request_body = parameters
215
+ logger.debug(f"Set request_body: {request_body}")
216
+ api_auth = os.getenv("API_AUTH_BEARER")
217
+ if api_auth:
218
+ headers["Authorization"] = f"{API_AUTH_TYPE} {api_auth}"
219
+ logger.debug(f"Added Authorization header with {API_AUTH_TYPE}")
220
+ logger.debug(f"Sending request - Method: {function_def['method']}, URL: {api_url}, Headers: {headers}, Params: {request_params}, Body: {request_body}")
221
+ try:
222
+ response = requests.request(
223
+ method=function_def["method"],
224
+ url=api_url,
225
+ headers=headers,
226
+ params=request_params if function_def["method"] == "GET" else None,
227
+ json=request_body if function_def["method"] != "GET" and request_body else None
228
+ )
229
+ response.raise_for_status()
230
+ logger.debug(f"API response received: {response.text}")
231
+ return response.text
232
+ except requests.exceptions.RequestException as e:
233
+ logger.error(f"API request failed: {e}", exc_info=True)
234
+ return json.dumps({"error": f"API request failed: {e}"})
235
+
236
+ def run_simple_server():
237
+ logger.debug("Starting run_simple_server")
238
+ spec_url = os.environ.get("OPENAPI_SPEC_URL")
239
+ if not spec_url:
240
+ logger.error("OPENAPI_SPEC_URL environment variable is required for FastMCP mode.")
241
+ sys.exit(1)
242
+ try:
243
+ logger.debug("Starting MCP server (FastMCP version)...")
244
+ mcp.run(transport="stdio")
245
+ except Exception as e:
246
+ logger.error("Unhandled exception in MCP server (FastMCP): %s", e, exc_info=True)
247
+ sys.exit(1)
248
+
249
+ if __name__ == "__main__":
250
+ run_simple_server()
@@ -0,0 +1,283 @@
1
+ """
2
+ Low-Level Server for mcp-openapi-proxy.
3
+
4
+ This server dynamically registers functions (tools) based on an OpenAPI specification,
5
+ directly utilizing the spec for tool definitions and invocation.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import asyncio
11
+ import json
12
+ import requests
13
+ from typing import List, Dict, Any
14
+ from mcp import types
15
+ from mcp.server.lowlevel import Server
16
+ from mcp.server.models import InitializationOptions
17
+ from mcp.server.stdio import stdio_server
18
+ from mcp_openapi_proxy.utils import setup_logging, fetch_openapi_spec, normalize_tool_name, is_tool_whitelisted
19
+
20
+ # Configure logging
21
+ DEBUG = os.getenv("DEBUG", "").lower() in ("true", "1", "yes")
22
+ logger = setup_logging(debug=DEBUG)
23
+
24
+ # Global function (tool) list
25
+ tools: List[types.Tool] = []
26
+ openapi_spec_data = None # Store OpenAPI spec globally (or use caching)
27
+
28
+ # Initialize the Low-Level MCP Server
29
+ mcp = Server("OpenApiProxy-LowLevel")
30
+
31
+
32
+ async def dispatcher_handler(request: types.CallToolRequest) -> types.ServerResult:
33
+ """
34
+ Dispatcher handler that routes CallToolRequest to the appropriate function (tool)
35
+ and makes the actual API call, now handling path parameters.
36
+ """
37
+ global openapi_spec_data
38
+
39
+ try:
40
+ function_name = request.params.name
41
+ logger.debug(f"Dispatcher received CallToolRequest for function: {function_name}")
42
+
43
+ tool = next((tool for tool in tools if tool.name == function_name), None)
44
+ if not tool:
45
+ logger.error(f"Unknown function requested: {function_name}")
46
+ return types.ServerResult(
47
+ root=types.CallToolResult(
48
+ content=[types.TextContent(type="text", text="Unknown function requested")]
49
+ )
50
+ )
51
+
52
+ arguments = request.params.arguments
53
+ logger.debug(f"Function arguments: {arguments}")
54
+
55
+ operation_details = lookup_operation_details(function_name, openapi_spec_data)
56
+ if not operation_details:
57
+ return types.ServerResult(
58
+ root=types.CallToolResult(
59
+ content=[types.TextContent(type="text", text=f"Could not find OpenAPI operation for function: {function_name}")]
60
+ )
61
+ )
62
+
63
+ path = operation_details['path']
64
+ method = operation_details['method']
65
+ operation = operation_details['operation']
66
+
67
+ # Construct API request URL
68
+ base_url = openapi_spec_data.get('servers', [{}])[0].get('url', '').rstrip('/')
69
+ api_url = base_url + path
70
+ path_params = {} # Dictionary to hold path parameters
71
+
72
+ # Extract path parameters from arguments and replace in URL
73
+ if arguments and 'parameters' in arguments:
74
+ path_params_in_openapi = [
75
+ param['name'] for param in operation.get('parameters', []) if param['in'] == 'path'
76
+ ]
77
+ for param_name in path_params_in_openapi:
78
+ if param_name in arguments['parameters']:
79
+ path_params[param_name] = arguments['parameters'].pop(param_name) # Remove from arguments as it's a path param
80
+ api_url = api_url.replace(f"{{{param_name}}}", str(path_params[param_name])) # Replace placeholder in URL
81
+
82
+ # Prepare remaining parameters as query parameters (after removing path params)
83
+ query_params = {}
84
+ headers = {}
85
+ auth_token = os.getenv("API_AUTH_BEARER")
86
+ if auth_token:
87
+ headers["Authorization"] = "Bearer " + auth_token
88
+ request_body = None
89
+
90
+ if arguments and 'parameters' in arguments: # 'parameters' now only contains query params (after path params removed)
91
+ query_params = arguments['parameters'].copy()
92
+
93
+ logger.debug(f"API Request URL: {api_url}")
94
+ logger.debug(f"Request Method: {method}")
95
+ logger.debug(f"Path Parameters: {path_params}")
96
+ logger.debug(f"Query Parameters: {query_params}")
97
+
98
+ try:
99
+ response = requests.request(
100
+ method=method,
101
+ url=api_url,
102
+ params=query_params if query_params else None,
103
+ headers=headers,
104
+ json=request_body if request_body else None
105
+ )
106
+ response.raise_for_status()
107
+
108
+ try:
109
+ response_data = response.json()
110
+ except json.JSONDecodeError:
111
+ response_data = response.text
112
+
113
+ return types.ServerResult(
114
+ root=types.CallToolResult(
115
+ content=[types.TextContent(type="text", text=json.dumps({
116
+ "status_code": response.status_code,
117
+ "headers": dict(response.headers),
118
+ "body": response_data
119
+ }, indent=2))]
120
+ )
121
+ )
122
+
123
+ except requests.exceptions.RequestException as e:
124
+ logger.error(f"API request failed: {e}")
125
+ return types.ServerResult(
126
+ root=types.CallToolResult(
127
+ content=[types.TextContent(type="text", text=json.dumps({
128
+ "error": f"API request failed: {e}"
129
+ }, indent=2))]
130
+ )
131
+ )
132
+
133
+ except Exception as e:
134
+ logger.error(f"Unhandled exception in dispatcher_handler: {e}", exc_info=True)
135
+ return types.ServerResult(
136
+ root=types.CallToolResult(
137
+ content=[types.TextContent(type="text", text=json.dumps({"error": "Internal server error."}, indent=2))]
138
+ )
139
+ )
140
+
141
+
142
+ async def list_tools(request: types.ListToolsRequest) -> types.ServerResult:
143
+ """
144
+ Handler for ListToolsRequest to list all registered functions (tools).
145
+ """
146
+ logger.debug("Handling list_tools request.")
147
+ return types.ServerResult(root=types.ListToolsResult(tools=tools))
148
+
149
+
150
+ def register_functions(spec: Dict) -> List[types.Tool]:
151
+ """
152
+ Register functions (tools) dynamically based on the OpenAPI specification.
153
+ No longer stores metadata in tool.metadata, relies on spec lookup.
154
+ """
155
+ global tools
156
+ tools = [] # Clear existing functions before re-registration
157
+
158
+ if not spec or 'paths' not in spec:
159
+ logger.warning("No paths found in OpenAPI spec, no functions registered.")
160
+ return tools
161
+
162
+ for path, path_item in spec['paths'].items():
163
+ if not is_tool_whitelisted(path):
164
+ logger.debug(f"Skipping non-whitelisted path: {path}")
165
+ continue
166
+ for method, operation in path_item.items():
167
+ if method.lower() not in ['get', 'post', 'put', 'delete', 'patch']:
168
+ continue # Skip OPTIONS, HEAD etc.
169
+
170
+ try:
171
+ # Create a function name from method and path
172
+ function_name = normalize_tool_name(f"{method.upper()} {path}") # Normalize function name
173
+
174
+ description = operation.get('summary', operation.get('description', 'No description available'))
175
+
176
+ # Define a generic input schema - you can expand on this later
177
+ input_schema = {
178
+ "type": "object",
179
+ "properties": {
180
+ "parameters": { # Generic 'parameters' field for now
181
+ "type": "object",
182
+ "description": "Parameters for the API call",
183
+ "additionalProperties": True # Allow any properties for parameters
184
+ }
185
+ },
186
+ "additionalProperties": False # No other top-level properties allowed
187
+ }
188
+
189
+ tool = types.Tool(
190
+ name=function_name,
191
+ description=description,
192
+ inputSchema=input_schema,
193
+ )
194
+ tools.append(tool)
195
+ logger.debug(f"Registered function: {function_name} ({method.upper()} {path})")
196
+
197
+ except Exception as e:
198
+ logger.error(f"Error registering function for {method.upper()} {path}: {e}")
199
+
200
+ logger.info(f"Registered {len(tools)} functions from OpenAPI spec.")
201
+ return tools
202
+
203
+
204
+ def lookup_operation_details(function_name: str, spec: Dict) -> Dict or None:
205
+ """
206
+ Lookup OpenAPI operation details (path, method, operation) based on function name.
207
+ """
208
+ if not spec or 'paths' not in spec:
209
+ return None
210
+
211
+ for path, path_item in spec['paths'].items():
212
+ for method, operation in path_item.items():
213
+ if method.lower() not in ['get', 'post', 'put', 'delete', 'patch']:
214
+ continue
215
+
216
+ current_function_name = normalize_tool_name(f"{method.upper()} {path}")
217
+ if current_function_name == function_name:
218
+ return {
219
+ "path": path,
220
+ "method": method.upper(),
221
+ "operation": operation
222
+ }
223
+ return None
224
+
225
+
226
+ async def start_server():
227
+ """
228
+ Start the Low-Level MCP server.
229
+ """
230
+ logger.debug("Starting Low-Level MCP server...")
231
+ try:
232
+ async with stdio_server() as (read_stream, write_stream):
233
+ await mcp.run(
234
+ read_stream,
235
+ write_stream,
236
+ initialization_options=InitializationOptions(
237
+ server_name="AnyOpenAPIMCP-LowLevel",
238
+ server_version="0.1.0",
239
+ capabilities=types.ServerCapabilities(),
240
+ ),
241
+ )
242
+ except Exception as e:
243
+ logger.critical(f"Unhandled exception in MCP server: {e}", e)
244
+ sys.exit(1)
245
+
246
+
247
+ def run_server():
248
+ """
249
+ Run the Low-Level Any OpenAPI server.
250
+ Fetches OpenAPI spec, registers functions, and makes it available globally.
251
+ """
252
+ global openapi_spec_data # To store it globally
253
+
254
+ try:
255
+ openapi_url = os.getenv('OPENAPI_SPEC_URL', 'https://raw.githubusercontent.com/seriousme/fastify-openapi-glue/refs/heads/master/examples/petstore/petstore-openapi.v3.json') # Example URL
256
+ openapi_spec_data = fetch_openapi_spec(openapi_url) # Fetch and store globally
257
+ if not openapi_spec_data:
258
+ raise ValueError("Failed to fetch or parse OpenAPI specification.")
259
+ logger.info("OpenAPI specification fetched successfully.")
260
+
261
+ register_functions(openapi_spec_data) # Register functions after fetching spec
262
+
263
+ except Exception as e:
264
+ logger.critical(f"Failed to start server: {e}")
265
+ sys.exit(1)
266
+
267
+ mcp.request_handlers[types.ListToolsRequest] = list_tools
268
+ logger.debug("Registered list_tools handler.")
269
+
270
+ mcp.request_handlers[types.CallToolRequest] = dispatcher_handler
271
+ logger.debug("Registered dispatcher_handler for CallToolRequest.")
272
+
273
+ try:
274
+ asyncio.run(start_server())
275
+ except KeyboardInterrupt:
276
+ logger.debug("MCP server shutdown initiated by user.")
277
+ except Exception as e:
278
+ logger.critical("Failed to start MCP server: %s", e)
279
+ sys.exit(1)
280
+
281
+
282
+ if __name__ == "__main__":
283
+ run_server()
@@ -0,0 +1,255 @@
1
+ """
2
+ Utility functions for mcp_openapi_proxy, including logging setup,
3
+ OpenAPI fetching, name normalization, and whitelist filtering for tools.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import logging
9
+ import requests
10
+ import re
11
+ import json
12
+ import yaml
13
+ from dotenv import load_dotenv
14
+
15
+ # Load environment variables from .env if present
16
+ load_dotenv()
17
+
18
+ OPENAPI_SPEC_URL = os.getenv("OPENAPI_SPEC_URL")
19
+
20
+ def setup_logging(debug: bool = False, log_dir: str = None, log_file: str = "debug-mcp-any-openapi.log") -> logging.Logger:
21
+ """
22
+ Sets up logging for the application, including outputting CRITICAL and ERROR logs to stdout.
23
+
24
+ Args:
25
+ debug (bool): If True, sets log level to DEBUG; otherwise, INFO.
26
+ log_dir (str): Directory where log files will be stored. Ignored if OPENAPI_LOGFILE_PATH is set.
27
+ log_file (str): Name of the log file. Ignored if OPENAPI_LOGFILE_PATH is set.
28
+
29
+ Returns:
30
+ logging.Logger: Configured logger instance.
31
+ """
32
+ log_path = os.getenv("OPENAPI_LOGFILE_PATH")
33
+ if not log_path:
34
+ if log_dir is None:
35
+ log_dir = os.path.join(os.path.expanduser("~"), "mcp_logs")
36
+ try:
37
+ os.makedirs(log_dir, exist_ok=True)
38
+ log_path = os.path.join(log_dir, log_file)
39
+ except PermissionError as e:
40
+ log_path = None
41
+ print(f"[ERROR] Failed to create log directory: {e}", file=sys.stderr)
42
+ logger = logging.getLogger(__name__)
43
+ logger.setLevel(logging.DEBUG if debug else logging.INFO)
44
+ logger.propagate = False
45
+ for handler in logger.handlers[:]:
46
+ logger.removeHandler(handler)
47
+ handlers = []
48
+ if log_path:
49
+ try:
50
+ file_handler = logging.FileHandler(log_path, mode="a")
51
+ file_handler.setLevel(logging.DEBUG if debug else logging.INFO)
52
+ formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(message)s")
53
+ file_handler.setFormatter(formatter)
54
+ handlers.append(file_handler)
55
+ except Exception as e:
56
+ print(f"[ERROR] Failed to create log file handler: {e}", file=sys.stderr)
57
+ try:
58
+ stdout_handler = logging.StreamHandler(sys.stdout)
59
+ stdout_handler.setLevel(logging.ERROR)
60
+ formatter = logging.Formatter("[%(levelname)s] %(message)s")
61
+ stdout_handler.setFormatter(formatter)
62
+ handlers.append(stdout_handler)
63
+ except Exception as e:
64
+ print(f"[ERROR] Failed to create stdout log handler: {e}", file=sys.stderr)
65
+ for handler in handlers:
66
+ logger.addHandler(handler)
67
+ if log_path:
68
+ logger.debug(f"Logging initialized. Writing logs to {log_path}")
69
+ else:
70
+ logger.debug("Logging initialized. Logs will only appear in stdout.")
71
+ return logger
72
+
73
+ DEBUG = os.getenv("DEBUG", "").lower() in ("true", "1", "yes")
74
+ logger = setup_logging(debug=DEBUG)
75
+
76
+ logger.debug(f"OpenAPI Spec URL: {OPENAPI_SPEC_URL}")
77
+ logger.debug("utils.py initialized")
78
+
79
+ def redact_api_key(key: str) -> str:
80
+ """
81
+ Redacts an API key for secure logging.
82
+
83
+ Args:
84
+ key (str): The API key or secret.
85
+
86
+ Returns:
87
+ str: The redacted API key, or '<not set>' if key is invalid.
88
+ """
89
+ if not key or len(key) <= 4:
90
+ return "<not set>"
91
+ return f"{key[:2]}{'*' * (len(key) - 4)}{key[-2:]}"
92
+
93
+ def normalize_tool_name(name: str) -> str:
94
+ """
95
+ Normalizes tool names by converting to lowercase and replacing non-alphanumeric characters with underscores.
96
+
97
+ Args:
98
+ name (str): The original tool name.
99
+
100
+ Returns:
101
+ str: A normalized tool name. Returns 'unknown_tool' if input is invalid.
102
+ """
103
+ logger = logging.getLogger(__name__)
104
+ if not name or not isinstance(name, str):
105
+ logger.warning(f"Invalid tool name input: {name}. Using default 'unknown_tool'.")
106
+ return "unknown_tool"
107
+ normalized = re.sub(r"[^a-zA-Z0-9_]", "_", name).lower()
108
+ normalized = re.sub(r"_+", "_", normalized)
109
+ normalized = normalized.strip('_')
110
+ logger.debug(f"Normalized tool name from '{name}' to '{normalized}'")
111
+ return normalized or "unknown_tool"
112
+
113
+ def get_tool_prefix() -> str:
114
+ """
115
+ Obtains the tool name prefix from the environment variable, ensuring it ends with an underscore.
116
+
117
+ Returns:
118
+ str: The tool name prefix.
119
+ """
120
+ prefix = os.getenv("TOOL_NAME_PREFIX", "")
121
+ if prefix and not prefix.endswith("_"):
122
+ prefix += "_"
123
+ return prefix
124
+
125
+ def is_tool_whitelisted(endpoint: str) -> bool:
126
+ """
127
+ Checks if an endpoint is in the TOOL_WHITELIST, supporting exact matches, prefixes, and path parameters.
128
+
129
+ Args:
130
+ endpoint (str): The API endpoint path to check.
131
+
132
+ Returns:
133
+ bool: True if whitelisted or no whitelist set, False otherwise.
134
+ """
135
+ whitelist = os.getenv("TOOL_WHITELIST", "")
136
+ logger.debug(f"Checking whitelist - endpoint: {endpoint}, TOOL_WHITELIST: {whitelist}")
137
+ if not whitelist:
138
+ logger.debug("No TOOL_WHITELIST set, allowing all endpoints.")
139
+ return True
140
+
141
+ whitelist_items = [item.strip() for item in whitelist.split(",") if item.strip()]
142
+
143
+ # Direct match
144
+ if endpoint in whitelist_items:
145
+ logger.debug(f"Direct match found for {endpoint} in whitelist")
146
+ return True
147
+
148
+ # Prefix match (e.g., /tasks matches /tasks/123)
149
+ for item in whitelist_items:
150
+ if not '{' in item and endpoint.startswith(item):
151
+ logger.debug(f"Prefix match found: {item} starts {endpoint}")
152
+ return True
153
+
154
+ # Path parameter match (e.g., /sessions/{sessionId} matches /sessions/abc123 or /sessions/abc123/items)
155
+ for item in whitelist_items:
156
+ if '{' in item and '}' in item:
157
+ pattern = re.escape(item)
158
+ pattern = pattern.replace(r"\{", "{").replace(r"\}", "}")
159
+ pattern = re.sub(r"\{[^}]+\}", r"[^/]+", pattern)
160
+ pattern = f"^{pattern}(/.*)?$" # Allow optional trailing segments
161
+ if re.match(pattern, endpoint):
162
+ logger.debug(f"Pattern match found for {endpoint} using {item}")
163
+ return True
164
+
165
+ logger.debug(f"No whitelist match found for {endpoint}")
166
+ return False
167
+
168
+ def fetch_openapi_spec(spec_url: str) -> dict:
169
+ """
170
+ Fetches and parses an OpenAPI specification from a URL or local file, supporting JSON and YAML.
171
+
172
+ Args:
173
+ spec_url (str): The URL or file path (e.g., file:///path/to/spec.json) of the OpenAPI spec.
174
+
175
+ Returns:
176
+ dict: The parsed OpenAPI specification, or None if an error occurs.
177
+ """
178
+ logger = logging.getLogger(__name__)
179
+ try:
180
+ if spec_url.startswith("file://"):
181
+ spec_path = spec_url.replace("file://", "")
182
+ with open(spec_path, 'r') as f:
183
+ content = f.read()
184
+ logger.debug(f"Read local OpenAPI spec from {spec_path}")
185
+ else:
186
+ response = requests.get(spec_url)
187
+ response.raise_for_status()
188
+ content = response.text
189
+ logger.debug(f"Fetched OpenAPI spec from {spec_url}")
190
+
191
+ if spec_url.endswith(('.yaml', '.yml')):
192
+ spec = yaml.safe_load(content)
193
+ logger.debug(f"Parsed YAML OpenAPI spec from {spec_url}")
194
+ else:
195
+ spec = json.loads(content)
196
+ logger.debug(f"Parsed JSON OpenAPI spec from {spec_url}")
197
+ return spec
198
+ except requests.exceptions.RequestException as e:
199
+ logger.error(f"Error fetching OpenAPI spec from {spec_url}: {e}")
200
+ return None
201
+ except (json.JSONDecodeError, yaml.YAMLError) as e:
202
+ logger.error(f"Error parsing OpenAPI spec from {spec_url}: {e}")
203
+ return None
204
+ except FileNotFoundError as e:
205
+ logger.error(f"Local file not found for OpenAPI spec at {spec_url}: {e}")
206
+ return None
207
+ except Exception as e:
208
+ logger.error(f"Unexpected error with OpenAPI spec from {spec_url}: {e}")
209
+ return None
210
+
211
+ def get_auth_type(spec: dict) -> str:
212
+ """
213
+ Determines the authentication type from the OpenAPI spec's securityDefinitions.
214
+
215
+ Args:
216
+ spec (dict): The OpenAPI specification.
217
+
218
+ Returns:
219
+ str: 'Api-Key' if apiKey auth is defined in Authorization header, 'Bearer' otherwise.
220
+ """
221
+ security_defs = spec.get("securityDefinitions", {})
222
+ for name, definition in security_defs.items():
223
+ if definition.get("type") == "apiKey" and definition.get("in") == "header" and definition.get("name") == "Authorization":
224
+ logger.debug(f"Detected ApiKeyAuth in spec: {name}")
225
+ return "Api-Key"
226
+ logger.debug("No ApiKeyAuth found in spec, defaulting to Bearer")
227
+ return "Bearer"
228
+
229
+ def map_schema_to_tools(schema: dict) -> list:
230
+ """
231
+ Maps a given schema to a list of MCP tools.
232
+
233
+ Args:
234
+ schema (dict): The schema containing a list of classes.
235
+
236
+ Returns:
237
+ list: A list of tool objects configured from the schema.
238
+ """
239
+ from mcp import types
240
+ tools = []
241
+ classes = schema.get("classes", [])
242
+ for entry in classes:
243
+ cls = entry.get("class", "")
244
+ if not cls:
245
+ continue
246
+ tool_name = normalize_tool_name(cls)
247
+ prefix = os.getenv("TOOL_NAME_PREFIX", "")
248
+ if prefix:
249
+ if not prefix.endswith("_"):
250
+ prefix += "_"
251
+ tool_name = prefix + tool_name
252
+ description = f"Tool for class {cls}: " + json.dumps(entry)
253
+ tool = types.Tool(name=tool_name, description=description, inputSchema={"type": "object"})
254
+ tools.append(tool)
255
+ return tools
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mhand
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.2
2
+ Name: mcp-openapi-proxy
3
+ Version: 0.1.1741340797
4
+ Summary: MCP server for exposing OpenAPI specifications as MCP tools.
5
+ Author-email: Matthew Hand <matthewhandau@gmail.com>
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: mcp[cli]>=1.2.0
9
+ Requires-Dist: python-dotenv>=1.0.1
10
+ Requires-Dist: requests>=2.25.0
11
+ Requires-Dist: fastapi>=0.100.0
12
+ Requires-Dist: pydantic>=2.0
13
+ Requires-Dist: prance>=23.6.21.0
14
+ Requires-Dist: openapi-spec-validator>=0.7.1
15
+
16
+ # mcp-openapi-proxy
17
+
18
+ **mcp-openapi-proxy** is a Python package implementing a Model Context Protocol (MCP) server that dynamically exposes REST APIs defined by OpenAPI specifications as MCP tools. This allows you to easily integrate any OpenAPI-described API into MCP-based workflows.
19
+
20
+ ## Overview
21
+
22
+ The package supports two operation modes:
23
+
24
+ - **Low-Level Mode (Default):** Dynamically registers tools for all API endpoints defined in an OpenAPI specification eg. /chat/completions becomes chat_completions().
25
+ - **FastMCP Mode (Simple Mode):** Provides a simplified mode for exposing specific pre-configured API endpoints as tools ie. list_functions() and call_functions().
26
+
27
+ ## Features
28
+
29
+ - **Dynamic Tool Generation:** Automatically creates MCP tools from OpenAPI endpoint definitions.
30
+ - **Simple Mode Option:** Offers a static configuration option with FastMCP mode.
31
+ - **OpenAPI Specification Support:** Works with OpenAPI v3 specifications, and potentially v2.
32
+ - **Flexible Filtering:** Supports filtering endpoints by tags, paths, methods, etc. (if implemented)
33
+ - **MCP Integration:** Integrates seamlessly with MCP ecosystems to invoke REST APIs as tools.
34
+
35
+ ## Installation
36
+
37
+ ### MCP Ecosystem Integration
38
+
39
+ Add **mcp-openapi-proxy** to your MCP ecosystem by configuring your `mcpServers`. Generic example:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "mcp-openapi-proxy": {
45
+ "command": "uvx",
46
+ "args": [
47
+ "--from",
48
+ "git+https://github.com/matthewhand/mcp-openapi-proxy",
49
+ "mcp-openapi-proxy"
50
+ ],
51
+ "env": {
52
+ "OPENAPI_SPEC_URL": "${OPENAPI_SPEC_URL}",
53
+ "API_AUTH_BEARER": "",
54
+ "TOOL_WHITELIST": "",
55
+ "TOOL_NAME_PREFIX": ""
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ For examples of real world APIs like GetZep, Open-Webui, Netlify, Vercel, etc refer to [examples](./examples).
63
+
64
+
65
+ ## Modes of Operation
66
+
67
+ ### FastMCP Mode (Simple Mode)
68
+
69
+ - **Enabled by:** Setting `OPENAPI_SIMPLE_MODE=true`
70
+ - **Description:** Exposes a pre-defined set of tools based on specific OpenAPI endpoints defined in code.
71
+ - **Configuration:** Requires environment variables for tool definitions.
72
+
73
+ ### Low-Level Mode (Default)
74
+
75
+ - **Description:** Dynamically registers all valid API endpoints from the provided OpenAPI specification as separate tools.
76
+ - **Tool Naming:** Tools are named based on normalized OpenAPI path and method.
77
+ - **Behavior:** Descriptions are generated from the OpenAPI operation summaries and descriptions.
78
+
79
+ ## Environment Variables
80
+
81
+ - `OPENAPI_SPEC_URL`: URL to the OpenAPI specification JSON file (required).
82
+ - `OPENAPI_LOGFILE_PATH`: (Optional) Path for the log file.
83
+ - `OPENAPI_SIMPLE_MODE`: (Optional) Set to `true` for FastMCP mode.
84
+ - `TOOL_WHITELIST`: (Optional) A prefix filter to select only certain tools.
85
+ - `TOOL_NAME_PREFIX`: (Optional) A string to prepend to all tool names when mapping.
86
+
87
+ ## Examples
88
+
89
+ ### OpenWebUI Example
90
+
91
+ **1. Confirming the OpenAPI Endpoint**
92
+
93
+ Run the following command to retrieve the OpenAPI specification:
94
+
95
+ ```bash
96
+ curl http://localhost:3000/openapi.json
97
+ ```
98
+
99
+ If the output is a valid OpenAPI JSON document, it confirms that the endpoint is working correctly.
100
+
101
+ **2. Configuring mcp-openapi-proxy**
102
+
103
+ In your MCP ecosystem configuration file, set the `OPENAPI_SPEC_URL` to the above endpoint. For example:
104
+
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "mcp-openapi-proxy": {
109
+ "command": "uvx",
110
+ "args": [
111
+ "--from",
112
+ "git+https://github.com/matthewhand/mcp-openapi-proxy",
113
+ "mcp-openapi-proxy"
114
+ ],
115
+ "env": {
116
+ "OPENAPI_SPEC_URL": "http://localhost:3000/openapi.json",
117
+ "SERVER_URL_OVERRIDE": "http://localhost:3000/v1",
118
+ "TOOL_WHITELIST": "/models,/chat/completion",
119
+ "API_AUTH_BEARER": "your_openwebui_token_here"
120
+ }
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ - **OPENAPI_SPEC_URL**: The URL to the OpenAPI specification.
127
+ - **SERVER_URL_OVERRIDE**: Should the spec not include servers, or you wish to use a different URL.
128
+ - **TOOL_WHITELIST**: A comma-separated list of endpoint paths to expose as tools. In this example, only the `/api/models` and `/api/chat/completions` endpoints are allowed.
129
+ - **API_AUTH_BEARER**: The bearer token for endpoints requiring authentication. Alternatively use ${OPENWEBUI_API_KEY} if configured as an environment variable or stored securely in a `.env` file.
130
+
131
+ **3. Resulting Tools**
132
+
133
+ With this configuration, the MCP server will dynamically generate tools for the whitelisted endpoints. For example:
134
+
135
+ ```json
136
+ [
137
+ {
138
+ "name": "api_models",
139
+ "description": "Fetches available models from /api/models"
140
+ },
141
+ {
142
+ "name": "api_chat_completions",
143
+ "description": "Generates chat completions via /api/chat/completions"
144
+ }
145
+ ]
146
+ ```
147
+
148
+ **4. Visual Verification**
149
+
150
+ You can verify the registration of these tools by inspecting the MCP server logs or using your MCP client.
151
+
152
+
153
+ ## Fly.io Example
154
+
155
+ **1. Verify the OpenAPI Endpoint**
156
+
157
+ Run the following command to retrieve the Fly.io OpenAPI JSON specification:
158
+
159
+ ```bash
160
+ curl https://raw.githubusercontent.com/abhiaagarwal/peristera/refs/heads/main/fly-machines-gen/fixed_spec.json
161
+ ```
162
+
163
+ Ensure the response is a valid OpenAPI JSON document containing an "openapi" field (e.g., "openapi": "3.0.0") and defined API paths.
164
+
165
+ **2. Configure mcp-openapi-proxy for Fly.io**
166
+
167
+ Update your MCP ecosystem configuration to point to the Fly.io endpoint. For example:
168
+
169
+ ```json
170
+ {
171
+ "mcpServers": {
172
+ "mcp-openapi-proxy": {
173
+ "command": "uvx",
174
+ "args": [
175
+ "--from",
176
+ "git+https://github.com/matthewhand/mcp-openapi-proxy",
177
+ "mcp-openapi-proxy"
178
+ ],
179
+ "env": {
180
+ "OPENAPI_SPEC_URL": "https://raw.githubusercontent.com/abhiaagarwal/peristera/refs/heads/main/fly-machines-gen/fixed_spec.json",
181
+ "SERVER_URL_OVERRIDE": "https://api.machines.dev",
182
+ "TOOL_WHITELIST": "/machines/list,/machines/start,/machines/status",
183
+ "API_AUTH_BEARER": "${FLY_API_TOKEN}"
184
+ }
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ **3. Resulting Tools**
191
+
192
+ With this configuration, the MCP server will dynamically generate tools for the whitelisted endpoints. For example:
193
+
194
+ ```json
195
+ [
196
+ {
197
+ "name": "machines_list",
198
+ "description": "Fetches machine listing from /machines/list"
199
+ },
200
+ {
201
+ "name": "machines_start",
202
+ "description": "Initiates machine start via /machines/start"
203
+ },
204
+ {
205
+ "name": "machines_status",
206
+ "description": "Retrieves machine status from /machines/status"
207
+ }
208
+ ]
209
+ ```
210
+
211
+ ## Troubleshooting
212
+
213
+ - **Missing OPENAPI_SPEC_URL:** Verify that the `OPENAPI_SPEC_URL` environment variable is set and points to a valid OpenAPI JSON file.
214
+ - **Invalid OpenAPI Spec:** Ensure the JSON specification complies with the OpenAPI standard.
215
+ - **Filtering Issues:** Check that `TOOL_WHITELIST` is correctly defined to filter the desired classes.
216
+ - **Logging:** Consult the logs (as defined by `MCP_OPENAPI_LOGFILE_PATH`) for debugging information.
217
+ - **Verify `uvx`** Run the server directly from the GitHub repository using `uvx`:
218
+
219
+ ```bash
220
+ uvx --from git+https://github.com/matthewhand/mcp-openapi-proxy mcp-openapi-proxy
221
+ ```
222
+
223
+ ## License
224
+
225
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,11 @@
1
+ mcp_openapi_proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mcp_openapi_proxy/__main__.py,sha256=VIc087EhndsDqeRU5uVMrtxZMMapxfDih1KlpouF6r8,1867
3
+ mcp_openapi_proxy/server_fastmcp.py,sha256=UEKz1vjX0FgkmCVl0Y9eFht4E-CfSd2MXSm004P8bVE,11400
4
+ mcp_openapi_proxy/server_lowlevel.py,sha256=3atPZpIPMRzul4UCny73ycZoNorEnv6j-lhhApIIwu4,10947
5
+ mcp_openapi_proxy/utils.py,sha256=3fgQi_IC5Cw8B57GRCDMwKqqidzDuaKawe3V5pxTonU,9495
6
+ mcp_openapi_proxy-0.1.1741340797.dist-info/LICENSE,sha256=PjkyMZfBImLXpO5eKb_sI__W1JMf1jL51n1O7H4a1I0,1062
7
+ mcp_openapi_proxy-0.1.1741340797.dist-info/METADATA,sha256=7UO-9toQSEOHNVtjMOPyr9SoxNoNs_g4CibxrZjj0CY,7976
8
+ mcp_openapi_proxy-0.1.1741340797.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
9
+ mcp_openapi_proxy-0.1.1741340797.dist-info/entry_points.txt,sha256=d_nfFSGK0wyUwv01H4wjtOI45eo8CZ-XdKAbJOvnhFU,70
10
+ mcp_openapi_proxy-0.1.1741340797.dist-info/top_level.txt,sha256=oLvmu0l9_s8gAkTCT5921baWF27hlPDr1i2SIopuOc8,18
11
+ mcp_openapi_proxy-0.1.1741340797.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-openapi-proxy = mcp_openapi_proxy.__main__:main
@@ -0,0 +1 @@
1
+ mcp_openapi_proxy