awslabs.openapi-mcp-server 0.1.1__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.
Files changed (38) hide show
  1. awslabs/__init__.py +16 -0
  2. awslabs/openapi_mcp_server/__init__.py +69 -0
  3. awslabs/openapi_mcp_server/api/__init__.py +18 -0
  4. awslabs/openapi_mcp_server/api/config.py +200 -0
  5. awslabs/openapi_mcp_server/auth/__init__.py +27 -0
  6. awslabs/openapi_mcp_server/auth/api_key_auth.py +185 -0
  7. awslabs/openapi_mcp_server/auth/auth_cache.py +190 -0
  8. awslabs/openapi_mcp_server/auth/auth_errors.py +206 -0
  9. awslabs/openapi_mcp_server/auth/auth_factory.py +146 -0
  10. awslabs/openapi_mcp_server/auth/auth_protocol.py +63 -0
  11. awslabs/openapi_mcp_server/auth/auth_provider.py +160 -0
  12. awslabs/openapi_mcp_server/auth/base_auth.py +218 -0
  13. awslabs/openapi_mcp_server/auth/basic_auth.py +171 -0
  14. awslabs/openapi_mcp_server/auth/bearer_auth.py +108 -0
  15. awslabs/openapi_mcp_server/auth/cognito_auth.py +538 -0
  16. awslabs/openapi_mcp_server/auth/register.py +100 -0
  17. awslabs/openapi_mcp_server/patch/__init__.py +17 -0
  18. awslabs/openapi_mcp_server/prompts/__init__.py +18 -0
  19. awslabs/openapi_mcp_server/prompts/generators/__init__.py +22 -0
  20. awslabs/openapi_mcp_server/prompts/generators/operation_prompts.py +642 -0
  21. awslabs/openapi_mcp_server/prompts/generators/workflow_prompts.py +257 -0
  22. awslabs/openapi_mcp_server/prompts/models.py +70 -0
  23. awslabs/openapi_mcp_server/prompts/prompt_manager.py +150 -0
  24. awslabs/openapi_mcp_server/server.py +511 -0
  25. awslabs/openapi_mcp_server/utils/__init__.py +18 -0
  26. awslabs/openapi_mcp_server/utils/cache_provider.py +249 -0
  27. awslabs/openapi_mcp_server/utils/config.py +35 -0
  28. awslabs/openapi_mcp_server/utils/error_handler.py +349 -0
  29. awslabs/openapi_mcp_server/utils/http_client.py +263 -0
  30. awslabs/openapi_mcp_server/utils/metrics_provider.py +503 -0
  31. awslabs/openapi_mcp_server/utils/openapi.py +217 -0
  32. awslabs/openapi_mcp_server/utils/openapi_validator.py +253 -0
  33. awslabs_openapi_mcp_server-0.1.1.dist-info/METADATA +418 -0
  34. awslabs_openapi_mcp_server-0.1.1.dist-info/RECORD +38 -0
  35. awslabs_openapi_mcp_server-0.1.1.dist-info/WHEEL +4 -0
  36. awslabs_openapi_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
  37. awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/LICENSE +175 -0
  38. awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,257 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Workflow prompt generation for OpenAPI specifications."""
15
+
16
+ from awslabs.openapi_mcp_server import logger
17
+ from awslabs.openapi_mcp_server.prompts.models import PromptArgument
18
+ from fastmcp.prompts.prompt import Prompt
19
+ from typing import Any, Dict, List
20
+
21
+
22
+ def identify_workflows(paths: Dict[str, Any]) -> List[Dict[str, Any]]:
23
+ """Identify common workflows from API paths."""
24
+ workflows = []
25
+
26
+ # Group operations by resource type
27
+ resource_operations = {}
28
+
29
+ for path, path_item in paths.items():
30
+ # Extract resource type from path
31
+ path_parts = path.strip('/').split('/')
32
+ resource_type = None
33
+
34
+ # Look for resource identifier in path
35
+ for part in path_parts:
36
+ if part and not part.startswith('{'):
37
+ resource_type = part
38
+ break
39
+
40
+ if not resource_type:
41
+ continue
42
+
43
+ # Initialize resource operations
44
+ if resource_type not in resource_operations:
45
+ resource_operations[resource_type] = {
46
+ 'list': None,
47
+ 'get': None,
48
+ 'create': None,
49
+ 'update': None,
50
+ 'delete': None,
51
+ 'search': None,
52
+ }
53
+
54
+ # Categorize operations
55
+ for method, operation in path_item.items():
56
+ if not isinstance(operation, dict):
57
+ continue
58
+
59
+ op_id = operation.get('operationId', '')
60
+ op_id_lower = op_id.lower()
61
+
62
+ # Categorize based on method and operation ID
63
+ if method == 'get':
64
+ if 'list' in op_id_lower or 'getall' in op_id_lower:
65
+ resource_operations[resource_type]['list'] = operation
66
+ elif 'search' in op_id_lower or 'find' in op_id_lower:
67
+ resource_operations[resource_type]['search'] = operation
68
+ else:
69
+ resource_operations[resource_type]['get'] = operation
70
+ elif method == 'post':
71
+ if 'create' in op_id_lower or 'add' in op_id_lower:
72
+ resource_operations[resource_type]['create'] = operation
73
+ elif method in ['put', 'patch']:
74
+ resource_operations[resource_type]['update'] = operation
75
+ elif method == 'delete':
76
+ resource_operations[resource_type]['delete'] = operation
77
+
78
+ # Identify List-Get-Update workflow
79
+ for resource_type, operations in resource_operations.items():
80
+ if operations['list'] and operations['get'] and operations['update']:
81
+ workflows.append(
82
+ {
83
+ 'name': f'{resource_type}_list_get_update',
84
+ 'type': 'list_get_update',
85
+ 'resource_type': resource_type,
86
+ 'operations': {
87
+ 'list': operations['list'],
88
+ 'get': operations['get'],
89
+ 'update': operations['update'],
90
+ },
91
+ }
92
+ )
93
+
94
+ # Identify Search-Create workflow
95
+ if operations['search'] and operations['create']:
96
+ workflows.append(
97
+ {
98
+ 'name': f'{resource_type}_search_create',
99
+ 'type': 'search_create',
100
+ 'resource_type': resource_type,
101
+ 'operations': {'search': operations['search'], 'create': operations['create']},
102
+ }
103
+ )
104
+
105
+ return workflows
106
+
107
+
108
+ def generate_workflow_documentation(workflow: Dict[str, Any]) -> str:
109
+ """Generate documentation for a workflow."""
110
+ workflow_type = workflow['type']
111
+ resource_type = workflow['resource_type']
112
+ operations = workflow['operations']
113
+
114
+ doc_lines = []
115
+
116
+ # Add title (concise)
117
+ doc_lines.append(
118
+ f'# {resource_type.capitalize()} {workflow_type.replace("_", " ").title()} Workflow'
119
+ )
120
+
121
+ # Add workflow steps
122
+ doc_lines.append('\n## Steps')
123
+
124
+ if workflow_type == 'list_get_update':
125
+ list_op_id = operations['list'].get('operationId', 'list')
126
+ get_op_id = operations['get'].get('operationId', 'get')
127
+ update_op_id = operations['update'].get('operationId', 'update')
128
+
129
+ doc_lines.append(f'\n1. List {resource_type}s using `{list_op_id}`')
130
+ doc_lines.append(f'2. Get a specific {resource_type} using `{get_op_id}`')
131
+ doc_lines.append(f'3. Update the {resource_type} using `{update_op_id}`')
132
+
133
+ # Add code example
134
+ doc_lines.append('\n## Example Code')
135
+ doc_lines.append('```python')
136
+ doc_lines.append(f'# List all {resource_type}s')
137
+ doc_lines.append(f'{resource_type}_list = await {list_op_id}()')
138
+ doc_lines.append(f'\n# Get a specific {resource_type}')
139
+ doc_lines.append(
140
+ f"{resource_type}_id = {resource_type}_list[0]['id'] # Example: use first item"
141
+ )
142
+ doc_lines.append(f'{resource_type}_details = await {get_op_id}({resource_type}_id)')
143
+ doc_lines.append(f'\n# Update the {resource_type}')
144
+ doc_lines.append('update_data = {')
145
+ doc_lines.append(' # Include required fields here')
146
+ doc_lines.append('}')
147
+ doc_lines.append(f'updated = await {update_op_id}({resource_type}_id, update_data)')
148
+ doc_lines.append('```')
149
+
150
+ elif workflow_type == 'search_create':
151
+ search_op_id = operations['search'].get('operationId', 'search')
152
+ create_op_id = operations['create'].get('operationId', 'create')
153
+
154
+ doc_lines.append(f'\n1. Search for {resource_type}s using `{search_op_id}`')
155
+ doc_lines.append(f'2. If not found, create a new {resource_type} using `{create_op_id}`')
156
+
157
+ # Add code example
158
+ doc_lines.append('\n## Example Code')
159
+ doc_lines.append('```python')
160
+ doc_lines.append(f'# Search for {resource_type}s')
161
+ doc_lines.append('search_criteria = {')
162
+ doc_lines.append(' # Include search parameters here')
163
+ doc_lines.append('}')
164
+ doc_lines.append(f'search_results = await {search_op_id}(**search_criteria)')
165
+ doc_lines.append('\n# Create if not found')
166
+ doc_lines.append('if not search_results:')
167
+ doc_lines.append(' create_data = {')
168
+ doc_lines.append(' # Include required fields here')
169
+ doc_lines.append(' }')
170
+ doc_lines.append(f' new_{resource_type} = await {create_op_id}(create_data)')
171
+ doc_lines.append('```')
172
+
173
+ return '\n'.join(doc_lines)
174
+
175
+
176
+ def create_workflow_prompt(server: Any, workflow: Dict[str, Any]) -> bool:
177
+ """Create and register a workflow prompt with the server.
178
+
179
+ Args:
180
+ server: MCP server instance
181
+ workflow: Workflow definition
182
+
183
+ Returns:
184
+ bool: True if prompt was registered successfully, False otherwise
185
+
186
+ """
187
+ try:
188
+ workflow_type = workflow['type']
189
+ resource_type = workflow['resource_type']
190
+
191
+ # Generate documentation
192
+ documentation = generate_workflow_documentation(workflow)
193
+
194
+ # Get operations from workflow
195
+ operations = workflow['operations']
196
+
197
+ # Extract arguments from workflow operations
198
+ workflow_args = []
199
+
200
+ # Add resource type as an argument
201
+ workflow_args.append(
202
+ PromptArgument(
203
+ name='resource_type',
204
+ description=f'The type of resource ({resource_type})',
205
+ required=False,
206
+ )
207
+ )
208
+
209
+ # Add operation-specific arguments
210
+ for op_type, operation in operations.items():
211
+ if operation and 'parameters' in operation:
212
+ for param in operation.get('parameters', []):
213
+ if param.get('required', False):
214
+ param_name = param.get('name', '')
215
+ param_desc = param.get('description', f'Parameter for {op_type} operation')
216
+
217
+ # Check if this parameter is already added
218
+ if not any(arg.name == param_name for arg in workflow_args):
219
+ workflow_args.append(
220
+ PromptArgument(
221
+ name=param_name,
222
+ description=param_desc,
223
+ required=False, # Optional in workflow context
224
+ )
225
+ )
226
+
227
+ # Create a function that returns messages for this workflow
228
+ def workflow_fn() -> List[Dict[str, Any]]:
229
+ # Create messages
230
+ messages = [{'role': 'user', 'content': {'type': 'text', 'text': documentation}}]
231
+
232
+ return messages
233
+
234
+ # Register the function as a prompt
235
+ if hasattr(server, '_prompt_manager'):
236
+ # Create tags based on workflow metadata
237
+ tags = {resource_type, workflow_type}
238
+
239
+ # Create a prompt from the function
240
+ prompt = Prompt.from_function(
241
+ fn=workflow_fn,
242
+ name=workflow['name'],
243
+ description=f'Execute a {workflow_type} workflow for {resource_type}',
244
+ tags=tags,
245
+ )
246
+
247
+ # Add the prompt to the server
248
+ server._prompt_manager.add_prompt(prompt)
249
+ logger.debug(f'Added workflow prompt: {workflow["name"]}')
250
+ return True
251
+ else:
252
+ logger.warning('Server does not have _prompt_manager')
253
+ return False
254
+
255
+ except Exception as e:
256
+ logger.warning(f'Failed to create workflow prompt: {e}')
257
+ return False
@@ -0,0 +1,70 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Data models for MCP prompts."""
15
+
16
+ from pydantic import BaseModel, Field
17
+ from typing import Any, Dict, List, Literal, Optional, Union
18
+
19
+
20
+ class PromptArgument(BaseModel):
21
+ """Argument for an MCP prompt."""
22
+
23
+ name: str = Field(..., description='Unique identifier for the argument')
24
+ description: Optional[str] = Field(None, description='Human-readable description')
25
+ required: bool = Field(False, description='Whether the argument is required')
26
+
27
+ def dict(self) -> Dict[str, Any]:
28
+ """Convert to dictionary representation."""
29
+ result = {'name': self.name, 'required': self.required}
30
+ if self.description:
31
+ result['description'] = self.description
32
+ return result
33
+
34
+
35
+ class ResourceContent(BaseModel):
36
+ """Content for a resource message."""
37
+
38
+ uri: str = Field(..., description='URI of the resource')
39
+ mimeType: str = Field('application/json', description='MIME type of the resource')
40
+ text: Optional[str] = Field(None, description='Text content of the resource')
41
+
42
+
43
+ class TextMessage(BaseModel):
44
+ """Text message content."""
45
+
46
+ type: Literal['text'] = Field('text', description='Type of message content')
47
+ text: str = Field(..., description='Text content')
48
+
49
+
50
+ class ResourceMessage(BaseModel):
51
+ """Resource message content."""
52
+
53
+ type: Literal['resource'] = Field('resource', description='Type of message content')
54
+ resource: ResourceContent = Field(..., description='Resource content')
55
+
56
+
57
+ class PromptMessage(BaseModel):
58
+ """Message in an MCP prompt."""
59
+
60
+ role: str = Field(..., description='Role of the message sender')
61
+ content: Union[TextMessage, ResourceMessage] = Field(..., description='Content of the message')
62
+
63
+
64
+ class MCPPrompt(BaseModel):
65
+ """MCP-compliant prompt definition."""
66
+
67
+ name: str = Field(..., description='Unique identifier for the prompt')
68
+ description: Optional[str] = Field(None, description='Human-readable description')
69
+ arguments: Optional[List[PromptArgument]] = Field(None, description='Arguments for the prompt')
70
+ messages: Optional[List[PromptMessage]] = Field(None, description='Messages in the prompt')
@@ -0,0 +1,150 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """MCP prompt manager for OpenAPI specifications."""
15
+
16
+ from awslabs.openapi_mcp_server import logger
17
+ from awslabs.openapi_mcp_server.prompts.generators.operation_prompts import create_operation_prompt
18
+ from awslabs.openapi_mcp_server.prompts.generators.workflow_prompts import (
19
+ create_workflow_prompt,
20
+ identify_workflows,
21
+ )
22
+ from typing import Any, Dict
23
+
24
+
25
+ class MCPPromptManager:
26
+ """Manager for MCP-compliant prompts."""
27
+
28
+ def __init__(self):
29
+ """Initialize the prompt manager."""
30
+ self.prompts = []
31
+ self.resource_handlers = {}
32
+
33
+ async def generate_prompts(
34
+ self, server: Any, api_name: str, openapi_spec: Dict[str, Any]
35
+ ) -> Dict[str, bool]:
36
+ """Generate MCP-compliant prompts from an OpenAPI specification.
37
+
38
+ Args:
39
+ server: MCP server instance
40
+ api_name: Name of the API
41
+ openapi_spec: OpenAPI specification
42
+
43
+ Returns:
44
+ Status of prompt generation
45
+
46
+ """
47
+ logger.info(f'Generating MCP prompts for {api_name}')
48
+
49
+ # Extract API information
50
+ paths = openapi_spec.get('paths', {})
51
+
52
+ # Track generation status
53
+ status = {'operation_prompts_generated': False, 'workflow_prompts_generated': False}
54
+
55
+ # Generate operation prompts
56
+ operation_count = 0
57
+
58
+ for path, path_item in paths.items():
59
+ for method, operation in path_item.items():
60
+ if method not in ['get', 'post', 'put', 'patch', 'delete']:
61
+ continue
62
+
63
+ operation_id = operation.get('operationId')
64
+ if not operation_id:
65
+ continue
66
+
67
+ # Create and register operation prompt
68
+ success = create_operation_prompt(
69
+ server=server,
70
+ api_name=api_name,
71
+ operation_id=operation_id,
72
+ method=method,
73
+ path=path,
74
+ summary=operation.get('summary', ''),
75
+ description=operation.get('description', ''),
76
+ parameters=operation.get('parameters', []),
77
+ request_body=operation.get('requestBody'),
78
+ responses=operation.get('responses', {}),
79
+ security=operation.get('security', []),
80
+ paths=paths,
81
+ )
82
+
83
+ if success:
84
+ operation_count += 1
85
+
86
+ status['operation_prompts_generated'] = operation_count > 0
87
+ logger.info(f'Generated {operation_count} operation prompts')
88
+
89
+ # Generate workflow prompts
90
+ workflows = identify_workflows(paths)
91
+ workflow_count = 0
92
+
93
+ for workflow in workflows:
94
+ # Create and register workflow prompt
95
+ success = create_workflow_prompt(server, workflow)
96
+ if success:
97
+ workflow_count += 1
98
+
99
+ status['workflow_prompts_generated'] = workflow_count > 0
100
+ logger.info(f'Generated {workflow_count} workflow prompts')
101
+
102
+ return status
103
+
104
+ def register_api_resource_handler(self, server: Any, api_name: str, client: Any) -> None:
105
+ """Register a handler for API resources.
106
+
107
+ Args:
108
+ server: MCP server instance
109
+ api_name: Name of the API
110
+ client: HTTP client for making API requests
111
+
112
+ """
113
+
114
+ async def api_resource_handler(uri: str, params: Dict[str, Any]) -> Dict[str, Any]:
115
+ """Handle API resource requests."""
116
+ # Extract path from URI
117
+ # Format: api://api_name/path/to/resource
118
+ path = uri.split(f'api://{api_name}')[1]
119
+
120
+ # Substitute path parameters
121
+ for param_name, param_value in params.items():
122
+ path = path.replace(f'{{{param_name}}}', str(param_value))
123
+
124
+ try:
125
+ # Make the API request using the authenticated client
126
+ response = await client.get(path)
127
+ response.raise_for_status()
128
+
129
+ # Return the response
130
+ return {
131
+ 'text': response.text,
132
+ 'mimeType': response.headers.get('Content-Type', 'application/json'),
133
+ }
134
+ except Exception as e:
135
+ logger.error(f'Error accessing API resource {uri}: {e}')
136
+ return {'text': f'Error: {str(e)}', 'mimeType': 'text/plain'}
137
+
138
+ # Store the resource handler for later use
139
+ resource_uri = f'api://{api_name}/'
140
+ self.resource_handlers[resource_uri] = api_resource_handler
141
+
142
+ # Try to register the resource handler if the server supports it
143
+ try:
144
+ if hasattr(server, 'register_resource_handler'):
145
+ server.register_resource_handler(resource_uri, api_resource_handler)
146
+ logger.debug(f'Registered resource handler for {resource_uri}')
147
+ else:
148
+ logger.debug(f'Stored resource handler locally for {resource_uri}')
149
+ except Exception as e:
150
+ logger.warning(f'Failed to register resource handler: {e}')