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.
- awslabs/__init__.py +16 -0
- awslabs/openapi_mcp_server/__init__.py +69 -0
- awslabs/openapi_mcp_server/api/__init__.py +18 -0
- awslabs/openapi_mcp_server/api/config.py +200 -0
- awslabs/openapi_mcp_server/auth/__init__.py +27 -0
- awslabs/openapi_mcp_server/auth/api_key_auth.py +185 -0
- awslabs/openapi_mcp_server/auth/auth_cache.py +190 -0
- awslabs/openapi_mcp_server/auth/auth_errors.py +206 -0
- awslabs/openapi_mcp_server/auth/auth_factory.py +146 -0
- awslabs/openapi_mcp_server/auth/auth_protocol.py +63 -0
- awslabs/openapi_mcp_server/auth/auth_provider.py +160 -0
- awslabs/openapi_mcp_server/auth/base_auth.py +218 -0
- awslabs/openapi_mcp_server/auth/basic_auth.py +171 -0
- awslabs/openapi_mcp_server/auth/bearer_auth.py +108 -0
- awslabs/openapi_mcp_server/auth/cognito_auth.py +538 -0
- awslabs/openapi_mcp_server/auth/register.py +100 -0
- awslabs/openapi_mcp_server/patch/__init__.py +17 -0
- awslabs/openapi_mcp_server/prompts/__init__.py +18 -0
- awslabs/openapi_mcp_server/prompts/generators/__init__.py +22 -0
- awslabs/openapi_mcp_server/prompts/generators/operation_prompts.py +642 -0
- awslabs/openapi_mcp_server/prompts/generators/workflow_prompts.py +257 -0
- awslabs/openapi_mcp_server/prompts/models.py +70 -0
- awslabs/openapi_mcp_server/prompts/prompt_manager.py +150 -0
- awslabs/openapi_mcp_server/server.py +511 -0
- awslabs/openapi_mcp_server/utils/__init__.py +18 -0
- awslabs/openapi_mcp_server/utils/cache_provider.py +249 -0
- awslabs/openapi_mcp_server/utils/config.py +35 -0
- awslabs/openapi_mcp_server/utils/error_handler.py +349 -0
- awslabs/openapi_mcp_server/utils/http_client.py +263 -0
- awslabs/openapi_mcp_server/utils/metrics_provider.py +503 -0
- awslabs/openapi_mcp_server/utils/openapi.py +217 -0
- awslabs/openapi_mcp_server/utils/openapi_validator.py +253 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/METADATA +418 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/RECORD +38 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/WHEEL +4 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/LICENSE +175 -0
- awslabs_openapi_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
"""Generators for MCP prompts."""
|
|
15
|
+
|
|
16
|
+
from awslabs.openapi_mcp_server.prompts.generators.operation_prompts import create_operation_prompt
|
|
17
|
+
from awslabs.openapi_mcp_server.prompts.generators.workflow_prompts import (
|
|
18
|
+
identify_workflows,
|
|
19
|
+
create_workflow_prompt,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = ['create_operation_prompt', 'identify_workflows', 'create_workflow_prompt']
|
|
@@ -0,0 +1,642 @@
|
|
|
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
|
+
"""Operation prompt generation for OpenAPI specifications."""
|
|
15
|
+
|
|
16
|
+
import inspect
|
|
17
|
+
from awslabs.openapi_mcp_server import logger
|
|
18
|
+
from awslabs.openapi_mcp_server.prompts.models import (
|
|
19
|
+
PromptArgument,
|
|
20
|
+
)
|
|
21
|
+
from fastmcp.prompts.prompt import Prompt
|
|
22
|
+
from fastmcp.prompts.prompt import PromptArgument as FastMCPPromptArgument
|
|
23
|
+
from fastmcp.server.openapi import RouteType
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_enum_values(enum_values: List[Any], max_inline: int = 4) -> str:
|
|
28
|
+
"""Format enum values in a token-efficient way.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
enum_values: List of enum values
|
|
32
|
+
max_inline: Maximum number of values to include inline
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Formatted enum string
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
if not enum_values:
|
|
39
|
+
return ''
|
|
40
|
+
|
|
41
|
+
# Handle short lists
|
|
42
|
+
if len(enum_values) <= max_inline:
|
|
43
|
+
# Format each value based on its type
|
|
44
|
+
formatted_values = []
|
|
45
|
+
for v in enum_values:
|
|
46
|
+
if isinstance(v, str):
|
|
47
|
+
formatted_values.append(f'"{v}"')
|
|
48
|
+
else:
|
|
49
|
+
formatted_values.append(str(v))
|
|
50
|
+
return f'({", ".join(formatted_values)})'
|
|
51
|
+
else:
|
|
52
|
+
# For long lists, just show count
|
|
53
|
+
return f'({len(enum_values)} possible values)'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def extract_prompt_arguments(
|
|
57
|
+
parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]] = None
|
|
58
|
+
) -> List[PromptArgument]:
|
|
59
|
+
"""Extract prompt arguments from operation parameters and request body."""
|
|
60
|
+
arguments = []
|
|
61
|
+
used_names = set()
|
|
62
|
+
|
|
63
|
+
# Process path and query parameters
|
|
64
|
+
for param in parameters:
|
|
65
|
+
if param.get('in') in ['path', 'query']:
|
|
66
|
+
name = param.get('name', '')
|
|
67
|
+
|
|
68
|
+
# Skip if we've already processed a parameter with this name
|
|
69
|
+
if name in used_names:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
used_names.add(name)
|
|
73
|
+
|
|
74
|
+
# Create concise description
|
|
75
|
+
description = param.get('description', '')
|
|
76
|
+
|
|
77
|
+
# Add enum values if available (token-efficient format)
|
|
78
|
+
schema = param.get('schema', {})
|
|
79
|
+
|
|
80
|
+
# Add default value if available
|
|
81
|
+
if schema and 'default' in schema:
|
|
82
|
+
default_value = schema['default']
|
|
83
|
+
default_str = (
|
|
84
|
+
f'"{default_value}"' if isinstance(default_value, str) else str(default_value)
|
|
85
|
+
)
|
|
86
|
+
if description:
|
|
87
|
+
description += f'\nDefault: {default_str}'
|
|
88
|
+
else:
|
|
89
|
+
description = f'Default: {default_str}'
|
|
90
|
+
|
|
91
|
+
# Add enum values
|
|
92
|
+
if schema and 'enum' in schema:
|
|
93
|
+
enum_values = schema['enum']
|
|
94
|
+
enum_str = format_enum_values(enum_values)
|
|
95
|
+
|
|
96
|
+
# Add enum values to description
|
|
97
|
+
if description:
|
|
98
|
+
description += f'\nAllowed values: {enum_str}'
|
|
99
|
+
else:
|
|
100
|
+
description = f'Allowed values: {enum_str}'
|
|
101
|
+
|
|
102
|
+
arguments.append(
|
|
103
|
+
PromptArgument(
|
|
104
|
+
name=name,
|
|
105
|
+
description=description
|
|
106
|
+
or None, # Use None instead of empty string for description
|
|
107
|
+
required=param.get('required', False),
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Process request body if present
|
|
112
|
+
if request_body and 'content' in request_body:
|
|
113
|
+
for content_type, content_schema in request_body['content'].items():
|
|
114
|
+
schema = content_schema.get('schema', {})
|
|
115
|
+
if schema and schema.get('type') == 'object' and 'properties' in schema:
|
|
116
|
+
required_fields = schema.get('required', [])
|
|
117
|
+
|
|
118
|
+
# Process each property
|
|
119
|
+
for prop_name, prop_schema in schema['properties'].items():
|
|
120
|
+
# Skip if we've already processed a parameter with this name
|
|
121
|
+
if prop_name in used_names:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
used_names.add(prop_name)
|
|
125
|
+
|
|
126
|
+
# Create description
|
|
127
|
+
description = prop_schema.get('description', '')
|
|
128
|
+
|
|
129
|
+
# Add default value if available
|
|
130
|
+
if 'default' in prop_schema:
|
|
131
|
+
default_value = prop_schema['default']
|
|
132
|
+
default_str = (
|
|
133
|
+
f'"{default_value}"'
|
|
134
|
+
if isinstance(default_value, str)
|
|
135
|
+
else str(default_value)
|
|
136
|
+
)
|
|
137
|
+
if description:
|
|
138
|
+
description += f'\nDefault: {default_str}'
|
|
139
|
+
else:
|
|
140
|
+
description = f'Default: {default_str}'
|
|
141
|
+
|
|
142
|
+
# Add enum values if available
|
|
143
|
+
if 'enum' in prop_schema:
|
|
144
|
+
enum_values = prop_schema['enum']
|
|
145
|
+
enum_str = format_enum_values(enum_values)
|
|
146
|
+
|
|
147
|
+
# Add enum values to description
|
|
148
|
+
if description:
|
|
149
|
+
description += f'\nAllowed values: {enum_str}'
|
|
150
|
+
else:
|
|
151
|
+
description = f'Allowed values: {enum_str}'
|
|
152
|
+
|
|
153
|
+
# Check if this property is required
|
|
154
|
+
is_required = prop_name in required_fields
|
|
155
|
+
|
|
156
|
+
arguments.append(
|
|
157
|
+
PromptArgument(
|
|
158
|
+
name=prop_name,
|
|
159
|
+
description=description
|
|
160
|
+
or None, # Use None instead of empty string for description
|
|
161
|
+
required=is_required,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return arguments
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def determine_operation_type(server: Any, path: str, method: str) -> str:
|
|
169
|
+
"""Determine if an operation is mapped as a resource or tool."""
|
|
170
|
+
# Default to tool if we can't determine
|
|
171
|
+
operation_type = 'tool'
|
|
172
|
+
|
|
173
|
+
# Check if server has route mappings
|
|
174
|
+
if hasattr(server, '_openapi_router') and hasattr(server._openapi_router, '_routes'):
|
|
175
|
+
routes = server._openapi_router._routes
|
|
176
|
+
|
|
177
|
+
# Look for a matching route
|
|
178
|
+
for route in routes:
|
|
179
|
+
route_path = getattr(route, 'path', '')
|
|
180
|
+
route_method = getattr(route, 'method', '')
|
|
181
|
+
route_type = getattr(route, 'route_type', None)
|
|
182
|
+
|
|
183
|
+
# Check if this route matches our operation
|
|
184
|
+
if route_path == path and route_method.upper() == method.upper() and route_type:
|
|
185
|
+
# Convert RouteType enum to string
|
|
186
|
+
if route_type == RouteType.RESOURCE:
|
|
187
|
+
operation_type = 'resource'
|
|
188
|
+
elif route_type == RouteType.RESOURCE_TEMPLATE:
|
|
189
|
+
operation_type = 'resource_template'
|
|
190
|
+
elif route_type == RouteType.TOOL:
|
|
191
|
+
operation_type = 'tool'
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
return operation_type
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def determine_mime_type(responses: Optional[Dict[str, Any]]) -> str:
|
|
198
|
+
"""Determine the MIME type for an operation response."""
|
|
199
|
+
# Default to application/json
|
|
200
|
+
mime_type = 'application/json'
|
|
201
|
+
|
|
202
|
+
# Check responses section
|
|
203
|
+
if responses:
|
|
204
|
+
for status_code, response in responses.items():
|
|
205
|
+
if status_code.startswith('2') and 'content' in response:
|
|
206
|
+
mime_type = next(iter(response['content'].keys()), mime_type)
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
return mime_type
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def generate_operation_documentation(
|
|
213
|
+
operation_id: str,
|
|
214
|
+
method: str,
|
|
215
|
+
path: str,
|
|
216
|
+
summary: str,
|
|
217
|
+
description: str,
|
|
218
|
+
parameters: List[Dict[str, Any]],
|
|
219
|
+
request_body: Optional[Dict[str, Any]] = None,
|
|
220
|
+
responses: Optional[Dict[str, Any]] = None,
|
|
221
|
+
security: Optional[List[Dict[str, List[str]]]] = None,
|
|
222
|
+
) -> str:
|
|
223
|
+
"""Generate documentation for an operation."""
|
|
224
|
+
doc_lines = []
|
|
225
|
+
|
|
226
|
+
# Add title (operation ID only)
|
|
227
|
+
doc_lines.append(f'# {operation_id}')
|
|
228
|
+
|
|
229
|
+
# Add summary or description (not both, to save tokens)
|
|
230
|
+
if summary:
|
|
231
|
+
doc_lines.append(f'\n{summary}')
|
|
232
|
+
elif description:
|
|
233
|
+
doc_lines.append(f'\n{description}')
|
|
234
|
+
|
|
235
|
+
# Add method and path (token-efficient format)
|
|
236
|
+
doc_lines.append(f'\n**{method.upper()}** `{path}`')
|
|
237
|
+
|
|
238
|
+
# Add authentication requirements if present
|
|
239
|
+
if security:
|
|
240
|
+
auth_schemes = []
|
|
241
|
+
for sec_req in security:
|
|
242
|
+
for scheme, scopes in sec_req.items():
|
|
243
|
+
scope_text = f' ({", ".join(scopes)})' if scopes else ''
|
|
244
|
+
auth_schemes.append(f'{scheme}{scope_text}')
|
|
245
|
+
|
|
246
|
+
if auth_schemes:
|
|
247
|
+
doc_lines.append(f'\n**Auth**: {", ".join(auth_schemes)}')
|
|
248
|
+
|
|
249
|
+
# Add parameters section (only if parameters exist)
|
|
250
|
+
if parameters:
|
|
251
|
+
# Group parameters by location
|
|
252
|
+
path_params = [p for p in parameters if p.get('in') == 'path']
|
|
253
|
+
query_params = [p for p in parameters if p.get('in') == 'query']
|
|
254
|
+
|
|
255
|
+
# Add path parameters (concise format)
|
|
256
|
+
if path_params:
|
|
257
|
+
doc_lines.append('\n**Path parameters:**')
|
|
258
|
+
for param in path_params:
|
|
259
|
+
name = param.get('name', '')
|
|
260
|
+
required = '*' if param.get('required', False) else ''
|
|
261
|
+
|
|
262
|
+
# Add enum values inline if available
|
|
263
|
+
schema = param.get('schema', {})
|
|
264
|
+
enum_str = ''
|
|
265
|
+
if schema and 'enum' in schema:
|
|
266
|
+
enum_values = schema['enum']
|
|
267
|
+
enum_str = ' ' + format_enum_values(enum_values)
|
|
268
|
+
|
|
269
|
+
doc_lines.append(f'- {name}{required}{enum_str}')
|
|
270
|
+
|
|
271
|
+
# Add query parameters (concise format)
|
|
272
|
+
if query_params:
|
|
273
|
+
doc_lines.append('\n**Query parameters:**')
|
|
274
|
+
for param in query_params:
|
|
275
|
+
name = param.get('name', '')
|
|
276
|
+
required = '*' if param.get('required', False) else ''
|
|
277
|
+
|
|
278
|
+
# Add enum values inline if available
|
|
279
|
+
schema = param.get('schema', {})
|
|
280
|
+
enum_str = ''
|
|
281
|
+
if schema and 'enum' in schema:
|
|
282
|
+
enum_values = schema['enum']
|
|
283
|
+
enum_str = ' ' + format_enum_values(enum_values)
|
|
284
|
+
|
|
285
|
+
doc_lines.append(f'- {name}{required}{enum_str}')
|
|
286
|
+
|
|
287
|
+
# Add request body section with enum handling
|
|
288
|
+
if request_body and 'content' in request_body:
|
|
289
|
+
doc_lines.append(
|
|
290
|
+
'\n**Request body:** Required'
|
|
291
|
+
if request_body.get('required')
|
|
292
|
+
else '\n**Request body:** Optional'
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Add schema information if available
|
|
296
|
+
content = next(iter(request_body.get('content', {}).items()), None)
|
|
297
|
+
if content:
|
|
298
|
+
content_type, content_schema = content
|
|
299
|
+
schema = content_schema.get('schema', {})
|
|
300
|
+
|
|
301
|
+
if schema and schema.get('type') == 'object' and 'properties' in schema:
|
|
302
|
+
required_fields = schema.get('required', [])
|
|
303
|
+
|
|
304
|
+
# Add required fields with enum values
|
|
305
|
+
if required_fields:
|
|
306
|
+
doc_lines.append('\n**Required fields:**')
|
|
307
|
+
for field in required_fields:
|
|
308
|
+
if field in schema['properties']:
|
|
309
|
+
prop_schema = schema['properties'][field]
|
|
310
|
+
|
|
311
|
+
# Add enum values if available
|
|
312
|
+
enum_str = ''
|
|
313
|
+
if 'enum' in prop_schema:
|
|
314
|
+
enum_values = prop_schema['enum']
|
|
315
|
+
enum_str = ' ' + format_enum_values(enum_values)
|
|
316
|
+
|
|
317
|
+
doc_lines.append(f'- {field}{enum_str}')
|
|
318
|
+
|
|
319
|
+
# Add response codes (only success and common errors)
|
|
320
|
+
if responses:
|
|
321
|
+
success_codes = [code for code in responses.keys() if code.startswith('2')]
|
|
322
|
+
error_codes = [code for code in responses.keys() if code.startswith(('4', '5'))]
|
|
323
|
+
|
|
324
|
+
if success_codes or error_codes:
|
|
325
|
+
doc_lines.append('\n**Responses:**')
|
|
326
|
+
|
|
327
|
+
# Add success codes
|
|
328
|
+
for code in success_codes[:1]: # Only first success code for token efficiency
|
|
329
|
+
doc_lines.append(f'- {code}: {responses[code].get("description", "Success")}')
|
|
330
|
+
|
|
331
|
+
# Add error codes (limited to common ones)
|
|
332
|
+
for code in error_codes[:2]: # Only first two error codes for token efficiency
|
|
333
|
+
doc_lines.append(f'- {code}: {responses[code].get("description", "Error")}')
|
|
334
|
+
|
|
335
|
+
# Add example usage
|
|
336
|
+
doc_lines.append('\n**Example usage:**')
|
|
337
|
+
doc_lines.append('```python')
|
|
338
|
+
|
|
339
|
+
# Create example based on operation type
|
|
340
|
+
if method.lower() == 'get':
|
|
341
|
+
# For GET operations
|
|
342
|
+
param_str = ''
|
|
343
|
+
if parameters:
|
|
344
|
+
required_params = [p for p in parameters if p.get('required')]
|
|
345
|
+
if required_params:
|
|
346
|
+
param_examples = []
|
|
347
|
+
for param in required_params:
|
|
348
|
+
name = param.get('name', '')
|
|
349
|
+
schema = param.get('schema', {})
|
|
350
|
+
|
|
351
|
+
# Use enum value as example if available
|
|
352
|
+
if schema and 'enum' in schema and schema['enum']:
|
|
353
|
+
example_value = (
|
|
354
|
+
f'"{schema["enum"][0]}"'
|
|
355
|
+
if isinstance(schema['enum'][0], str)
|
|
356
|
+
else schema['enum'][0]
|
|
357
|
+
)
|
|
358
|
+
param_examples.append(f'{name}={example_value}')
|
|
359
|
+
else:
|
|
360
|
+
param_examples.append(f'{name}="value"')
|
|
361
|
+
|
|
362
|
+
param_str = ', '.join(param_examples)
|
|
363
|
+
|
|
364
|
+
doc_lines.append(f'response = await {operation_id}({param_str})')
|
|
365
|
+
|
|
366
|
+
elif method.lower() == 'post':
|
|
367
|
+
# For POST operations
|
|
368
|
+
if request_body:
|
|
369
|
+
doc_lines.append('data = {')
|
|
370
|
+
|
|
371
|
+
# Add required fields with example values
|
|
372
|
+
content = next(iter(request_body.get('content', {}).items()), None)
|
|
373
|
+
if content:
|
|
374
|
+
content_type, content_schema = content
|
|
375
|
+
schema = content_schema.get('schema', {})
|
|
376
|
+
|
|
377
|
+
if schema and schema.get('type') == 'object' and 'properties' in schema:
|
|
378
|
+
required_fields = schema.get('required', [])
|
|
379
|
+
|
|
380
|
+
for field in required_fields:
|
|
381
|
+
if field in schema['properties']:
|
|
382
|
+
prop_schema = schema['properties'][field]
|
|
383
|
+
prop_type = prop_schema.get('type', 'string')
|
|
384
|
+
|
|
385
|
+
# Use enum value as example if available
|
|
386
|
+
if 'enum' in prop_schema and prop_schema['enum']:
|
|
387
|
+
if prop_type == 'string':
|
|
388
|
+
doc_lines.append(f' "{field}": "{prop_schema["enum"][0]}",')
|
|
389
|
+
else:
|
|
390
|
+
doc_lines.append(f' "{field}": {prop_schema["enum"][0]},')
|
|
391
|
+
else:
|
|
392
|
+
# Use type-appropriate example
|
|
393
|
+
if prop_type == 'string':
|
|
394
|
+
doc_lines.append(f' "{field}": "example",')
|
|
395
|
+
elif prop_type == 'integer' or prop_type == 'number':
|
|
396
|
+
doc_lines.append(f' "{field}": 0,')
|
|
397
|
+
elif prop_type == 'boolean':
|
|
398
|
+
doc_lines.append(f' "{field}": False,')
|
|
399
|
+
elif prop_type == 'array':
|
|
400
|
+
doc_lines.append(f' "{field}": [],')
|
|
401
|
+
elif prop_type == 'object':
|
|
402
|
+
doc_lines.append(f' "{field}": {{}},')
|
|
403
|
+
|
|
404
|
+
doc_lines.append('}')
|
|
405
|
+
doc_lines.append(f'response = await {operation_id}(data)')
|
|
406
|
+
else:
|
|
407
|
+
doc_lines.append(f'response = await {operation_id}()')
|
|
408
|
+
|
|
409
|
+
else:
|
|
410
|
+
# For other operations
|
|
411
|
+
param_str = ''
|
|
412
|
+
if parameters:
|
|
413
|
+
required_params = [p for p in parameters if p.get('required')]
|
|
414
|
+
if required_params:
|
|
415
|
+
param_examples = []
|
|
416
|
+
for param in required_params:
|
|
417
|
+
name = param.get('name', '')
|
|
418
|
+
schema = param.get('schema', {})
|
|
419
|
+
|
|
420
|
+
# Use enum value as example if available
|
|
421
|
+
if schema and 'enum' in schema and schema['enum']:
|
|
422
|
+
example_value = (
|
|
423
|
+
f'"{schema["enum"][0]}"'
|
|
424
|
+
if isinstance(schema['enum'][0], str)
|
|
425
|
+
else schema['enum'][0]
|
|
426
|
+
)
|
|
427
|
+
param_examples.append(f'{name}={example_value}')
|
|
428
|
+
else:
|
|
429
|
+
param_examples.append(f'{name}="value"')
|
|
430
|
+
|
|
431
|
+
param_str = ', '.join(param_examples)
|
|
432
|
+
|
|
433
|
+
doc_lines.append(f'response = await {operation_id}({param_str})')
|
|
434
|
+
|
|
435
|
+
doc_lines.append('```')
|
|
436
|
+
|
|
437
|
+
return '\n'.join(doc_lines)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def create_operation_prompt(
|
|
441
|
+
server: Any,
|
|
442
|
+
api_name: str,
|
|
443
|
+
operation_id: str,
|
|
444
|
+
method: str,
|
|
445
|
+
path: str,
|
|
446
|
+
summary: str,
|
|
447
|
+
description: str,
|
|
448
|
+
parameters: List[Dict[str, Any]],
|
|
449
|
+
request_body: Optional[Dict[str, Any]] = None,
|
|
450
|
+
responses: Optional[Dict[str, Any]] = None,
|
|
451
|
+
security: Optional[List[Dict[str, List[str]]]] = None,
|
|
452
|
+
paths: Optional[Dict[str, Any]] = None,
|
|
453
|
+
) -> bool:
|
|
454
|
+
"""Create and register an operation prompt with the server.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
server: MCP server instance
|
|
458
|
+
api_name: Name of the API
|
|
459
|
+
operation_id: Operation ID
|
|
460
|
+
method: HTTP method
|
|
461
|
+
path: API path
|
|
462
|
+
summary: Operation summary
|
|
463
|
+
description: Operation description
|
|
464
|
+
parameters: Operation parameters
|
|
465
|
+
request_body: Request body schema
|
|
466
|
+
responses: Response schemas
|
|
467
|
+
security: Security requirements
|
|
468
|
+
paths: OpenAPI paths object
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
bool: True if prompt was registered successfully, False otherwise
|
|
472
|
+
|
|
473
|
+
"""
|
|
474
|
+
try:
|
|
475
|
+
# Determine operation type
|
|
476
|
+
operation_type = determine_operation_type(server, path, method)
|
|
477
|
+
|
|
478
|
+
# Generate documentation
|
|
479
|
+
documentation = generate_operation_documentation(
|
|
480
|
+
operation_id=operation_id,
|
|
481
|
+
method=method,
|
|
482
|
+
path=path,
|
|
483
|
+
summary=summary,
|
|
484
|
+
description=description,
|
|
485
|
+
parameters=parameters,
|
|
486
|
+
request_body=request_body,
|
|
487
|
+
responses=responses,
|
|
488
|
+
security=security,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Extract arguments from parameters and request body
|
|
492
|
+
prompt_arguments = extract_prompt_arguments(parameters, request_body)
|
|
493
|
+
|
|
494
|
+
# Create a function that returns messages for this operation
|
|
495
|
+
# We need to create a function with the exact parameters we want to expose
|
|
496
|
+
# Instead of using exec(), we'll use a function factory approach
|
|
497
|
+
|
|
498
|
+
# Create a generic handler that will be wrapped with the correct signature
|
|
499
|
+
def generic_handler(doc, op_type, api_name_val, path_val, resp, args, *args_values):
|
|
500
|
+
"""Handle operation prompts generically."""
|
|
501
|
+
# Create a dictionary of parameter values
|
|
502
|
+
param_values = {}
|
|
503
|
+
for i, arg in enumerate(args):
|
|
504
|
+
if i < len(args_values):
|
|
505
|
+
param_values[arg.name] = args_values[i]
|
|
506
|
+
|
|
507
|
+
# Create messages
|
|
508
|
+
messages = [{'role': 'user', 'content': {'type': 'text', 'text': doc}}]
|
|
509
|
+
|
|
510
|
+
# For resources, add resource reference
|
|
511
|
+
if op_type in ['resource', 'resource_template']:
|
|
512
|
+
# Determine MIME type
|
|
513
|
+
mime_type = determine_mime_type(resp)
|
|
514
|
+
|
|
515
|
+
# Create resource URI
|
|
516
|
+
resource_uri = f'api://{api_name_val}{path_val}'
|
|
517
|
+
|
|
518
|
+
# Add resource reference message
|
|
519
|
+
messages.append(
|
|
520
|
+
{
|
|
521
|
+
'role': 'user',
|
|
522
|
+
'content': {
|
|
523
|
+
'type': 'resource',
|
|
524
|
+
'resource': {'uri': resource_uri, 'mimeType': mime_type},
|
|
525
|
+
},
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
logger.debug(f'Operation {operation_id} returning {len(messages)} messages')
|
|
530
|
+
return messages
|
|
531
|
+
|
|
532
|
+
# Create a function with the correct signature using functools.partial
|
|
533
|
+
from functools import partial
|
|
534
|
+
|
|
535
|
+
# Create a partial function with the fixed arguments
|
|
536
|
+
handler_with_fixed_args = partial(
|
|
537
|
+
generic_handler,
|
|
538
|
+
documentation,
|
|
539
|
+
operation_type,
|
|
540
|
+
api_name,
|
|
541
|
+
path,
|
|
542
|
+
responses,
|
|
543
|
+
prompt_arguments,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Define a function to create the appropriate operation function using inspect.Signature
|
|
547
|
+
def create_operation_function():
|
|
548
|
+
# Create a base function that will be wrapped with the correct signature
|
|
549
|
+
def base_fn(*args, **kwargs):
|
|
550
|
+
# Map positional args to their parameter names
|
|
551
|
+
param_names = [p.name for p in inspect.signature(base_fn).parameters.values()]
|
|
552
|
+
named_args = dict(zip(param_names, args))
|
|
553
|
+
named_args.update(kwargs)
|
|
554
|
+
|
|
555
|
+
# Extract the values in the correct order for handler_with_fixed_args
|
|
556
|
+
arg_values = []
|
|
557
|
+
for arg in prompt_arguments:
|
|
558
|
+
arg_values.append(named_args.get(arg.name))
|
|
559
|
+
|
|
560
|
+
return handler_with_fixed_args(*arg_values)
|
|
561
|
+
|
|
562
|
+
# Create parameters for the signature
|
|
563
|
+
# Sort arguments so required parameters come first, followed by optional parameters
|
|
564
|
+
required_args = [arg for arg in prompt_arguments if arg.required]
|
|
565
|
+
optional_args = [arg for arg in prompt_arguments if not arg.required]
|
|
566
|
+
|
|
567
|
+
# Create parameters list with required parameters first
|
|
568
|
+
parameters = []
|
|
569
|
+
|
|
570
|
+
# Add required parameters (no default value)
|
|
571
|
+
for arg in required_args:
|
|
572
|
+
param = inspect.Parameter(arg.name, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
573
|
+
parameters.append(param)
|
|
574
|
+
|
|
575
|
+
# Add optional parameters (with default=None)
|
|
576
|
+
for arg in optional_args:
|
|
577
|
+
param = inspect.Parameter(
|
|
578
|
+
arg.name, inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None
|
|
579
|
+
)
|
|
580
|
+
parameters.append(param)
|
|
581
|
+
|
|
582
|
+
# Create a new signature
|
|
583
|
+
sig = inspect.Signature(parameters, return_annotation=List[Dict[str, Any]])
|
|
584
|
+
|
|
585
|
+
# Apply the signature to the function
|
|
586
|
+
base_fn.__signature__ = sig
|
|
587
|
+
base_fn.__name__ = 'operation_fn'
|
|
588
|
+
base_fn.__doc__ = documentation
|
|
589
|
+
|
|
590
|
+
return base_fn
|
|
591
|
+
|
|
592
|
+
# Create the operation function
|
|
593
|
+
operation_fn = create_operation_function()
|
|
594
|
+
|
|
595
|
+
# Register the function as a prompt
|
|
596
|
+
if hasattr(server, '_prompt_manager'):
|
|
597
|
+
# Create tags based on operation metadata
|
|
598
|
+
tags = set()
|
|
599
|
+
# Get tags from the OpenAPI operation object if available
|
|
600
|
+
if isinstance(method, str) and paths is not None and path in paths:
|
|
601
|
+
path_item = paths.get(path, {})
|
|
602
|
+
if method.lower() in path_item:
|
|
603
|
+
op = path_item[method.lower()]
|
|
604
|
+
if 'tags' in op and isinstance(op.get('tags'), list):
|
|
605
|
+
for tag in op.get('tags', []):
|
|
606
|
+
if isinstance(tag, str):
|
|
607
|
+
tags.add(tag)
|
|
608
|
+
|
|
609
|
+
# Create a list of FastMCPPromptArgument objects for the Prompt
|
|
610
|
+
prompt_args = []
|
|
611
|
+
for arg in prompt_arguments:
|
|
612
|
+
# Use the actual parameter name from the OpenAPI schema
|
|
613
|
+
prompt_args.append(
|
|
614
|
+
FastMCPPromptArgument(
|
|
615
|
+
name=arg.name, description=arg.description, required=arg.required
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Create a prompt from the function
|
|
620
|
+
prompt = Prompt.from_function(
|
|
621
|
+
fn=operation_fn,
|
|
622
|
+
name=operation_id,
|
|
623
|
+
description=summary or description or f'{method.upper()} {path}',
|
|
624
|
+
tags=tags,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# Update the arguments with descriptions
|
|
628
|
+
prompt.arguments = prompt_args
|
|
629
|
+
|
|
630
|
+
# Add the prompt to the server
|
|
631
|
+
server._prompt_manager.add_prompt(prompt)
|
|
632
|
+
logger.debug(
|
|
633
|
+
f'Added operation prompt: {operation_id} with arguments: {[arg.name for arg in prompt.arguments]}'
|
|
634
|
+
)
|
|
635
|
+
return True
|
|
636
|
+
else:
|
|
637
|
+
logger.warning('Server does not have _prompt_manager')
|
|
638
|
+
return False
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.warning(f'Failed to create operation prompt: {e}')
|
|
642
|
+
return False
|