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,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