mcp-openapi-generator 1.0.0__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.
@@ -0,0 +1,13 @@
1
+ """MCP OpenAPI Generator - MCP server for OpenAPI code generation and validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "1.0.0"
6
+ __author__ = "M.K. Abdullahi"
7
+ __license__ = "MIT"
8
+
9
+ __all__ = [
10
+ "__version__",
11
+ "__author__",
12
+ "__license__",
13
+ ]
@@ -0,0 +1,198 @@
1
+ """CLI entry points for the MCP OpenAPI Generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.logging import RichHandler
14
+ from rich.table import Table
15
+
16
+ from .tools.generate import generate_client, list_generators
17
+ from .tools.validate import validate_spec
18
+ from .types import GeneratorTarget, SpecInput, SpecSource, ValidationTool
19
+ from .utils.config_inference import infer_project_config
20
+
21
+ __all__ = ["app", "generate_client", "validate_spec"]
22
+
23
+ app = typer.Typer(
24
+ name="mcp-openapi-generator",
25
+ help="MCP server for OpenAPI code generation and validation",
26
+ add_completion=False,
27
+ rich_markup_mode="rich",
28
+ )
29
+
30
+ console = Console()
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def _setup_logging(verbose: bool = False) -> None:
35
+ level = logging.DEBUG if verbose else logging.INFO
36
+ logging.basicConfig(
37
+ level=level,
38
+ format="%(message)s",
39
+ datefmt="[%X]",
40
+ handlers=[RichHandler(console=console, rich_tracebacks=True)],
41
+ )
42
+
43
+
44
+ def _parse_spec_input(spec: str, format: str = "yaml") -> SpecInput:
45
+ """Parse spec argument into SpecInput."""
46
+ if spec.startswith(("http://", "https://")):
47
+ return SpecInput(source=SpecSource.URL, location=spec, format=format)
48
+ if spec == "-":
49
+ return SpecInput(source=SpecSource.STDIN, location="-", format=format)
50
+ return SpecInput(source=SpecSource.FILE, location=spec, format=format)
51
+
52
+
53
+ @app.command()
54
+ def generate(
55
+ spec: Annotated[str, typer.Argument(help="OpenAPI spec URL, file path, or '-' for stdin")],
56
+ target: Annotated[
57
+ GeneratorTarget,
58
+ typer.Option("--target", "-t", help="Generation target language/framework"),
59
+ ] = GeneratorTarget.TYPESCRIPT_AXIOS,
60
+ output: Annotated[Path, typer.Option("--output", "-o", help="Output directory")] = Path("./generated"),
61
+ package_name: Annotated[str | None, typer.Option("--package-name", help="Package name for generated code")] = None,
62
+ package_version: Annotated[str | None, typer.Option("--package-version", help="Package version")] = None,
63
+ prop: Annotated[list[str], typer.Option("--prop", "-p", help="Additional properties (key=value)")] = [],
64
+ no_tests: Annotated[bool, typer.Option("--no-tests", help="Skip test generation")] = False,
65
+ no_post_process: Annotated[bool, typer.Option("--no-post-process", help="Skip formatting and test stubs")] = False,
66
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Verbose output")] = False,
67
+ ) -> None:
68
+ """Generate client code from an OpenAPI specification."""
69
+ _setup_logging(verbose)
70
+
71
+ spec_input = _parse_spec_input(spec)
72
+ additional_props = {}
73
+ for p in prop:
74
+ if "=" in p:
75
+ k, v = p.split("=", 1)
76
+ additional_props[k] = v
77
+
78
+ console.print(f"[bold]Generating client for {spec_input.location}[/bold]")
79
+ console.print(f"Target: {target.value}")
80
+ console.print(f"Output: {output}")
81
+
82
+ result = asyncio.run(
83
+ generate_client(
84
+ spec=spec_input,
85
+ target=target,
86
+ output_dir=output,
87
+ package_name=package_name,
88
+ additional_properties=additional_props,
89
+ generate_tests=not no_tests,
90
+ post_process=not no_post_process,
91
+ )
92
+ )
93
+
94
+ if result.success:
95
+ console.print(f"[green]✓[/green] {result.summary}")
96
+ console.print(f"Files created: {len(result.files_created)}")
97
+ if result.warnings:
98
+ for w in result.warnings:
99
+ console.print(f"[yellow]⚠[/yellow] {w}")
100
+ sys.exit(0)
101
+ else:
102
+ console.print(f"[red]✗[/red] Generation failed: {result.error}")
103
+ sys.exit(1)
104
+
105
+
106
+ @app.command()
107
+ def validate(
108
+ spec: Annotated[str, typer.Argument(help="OpenAPI spec URL, file path, or '-' for stdin")],
109
+ tool: Annotated[
110
+ ValidationTool,
111
+ typer.Option("--tool", help="Validation tool to use"),
112
+ ] = ValidationTool.SPECTRAL,
113
+ ruleset: Annotated[str | None, typer.Option("--ruleset", "-r", help="Custom ruleset path/URL")] = None,
114
+ fail_on_warnings: Annotated[bool, typer.Option("--fail-on-warnings", help="Fail if warnings found")] = False,
115
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Verbose output")] = False,
116
+ ) -> None:
117
+ """Validate an OpenAPI specification."""
118
+ _setup_logging(verbose)
119
+
120
+ spec_input = _parse_spec_input(spec)
121
+
122
+ console.print(f"[bold]Validating {spec_input.location}[/bold]")
123
+ console.print(f"Tool: {tool.value}")
124
+
125
+ result = asyncio.run(
126
+ validate_spec(
127
+ spec=spec_input,
128
+ tool=tool,
129
+ ruleset=ruleset,
130
+ fail_on_warnings=fail_on_warnings,
131
+ )
132
+ )
133
+
134
+ if result.success:
135
+ if result.valid:
136
+ console.print(f"[green]✓[/green] Specification is valid")
137
+ else:
138
+ console.print(f"[red]✗[/red] Specification invalid")
139
+ console.print(f"Errors: {len(result.errors)}, Warnings: {len(result.warnings)}")
140
+ for err in result.errors[:10]:
141
+ console.print(f" [red]Error[/red] {err.get('path', '')}: {err.get('message', '')}")
142
+ for warn in result.warnings[:5]:
143
+ console.print(f" [yellow]Warning[/yellow] {warn.get('path', '')}: {warn.get('message', '')}")
144
+ sys.exit(0 if result.valid else 1)
145
+ else:
146
+ console.print(f"[red]✗[/red] Validation failed: {result.error}")
147
+ sys.exit(1)
148
+
149
+
150
+ @app.command()
151
+ def generators() -> None:
152
+ """List available generators and targets."""
153
+ result = asyncio.run(list_generators())
154
+
155
+ console.print(f"[bold]Generator:[/bold] {result['generator']}")
156
+ console.print(f"[bold]Available:[/bold] {'Yes' if result['available'] else 'No'}")
157
+
158
+ if result["targets"]:
159
+ table = Table(title="Available Targets")
160
+ table.add_column("Target", style="cyan")
161
+ for target in sorted(result["targets"]):
162
+ table.add_row(target)
163
+ console.print(table)
164
+ else:
165
+ console.print("[yellow]No targets found[/yellow]")
166
+
167
+
168
+ @app.command()
169
+ def infer() -> None:
170
+ """Infer project configuration and suggest target."""
171
+ config = infer_project_config()
172
+ target = config.primary_language
173
+
174
+ table = Table(title="Project Configuration")
175
+ table.add_column("Property", style="cyan")
176
+ table.add_column("Value", style="green")
177
+
178
+ table.add_row("Language", config.language)
179
+ table.add_row("Framework", config.framework or "auto")
180
+ table.add_row("Package Manager", config.package_manager or "unknown")
181
+ table.add_row("Has package.json", str(config.has_package_json))
182
+ table.add_row("Has pyproject.toml", str(config.has_pyproject_toml))
183
+ table.add_row("Has Cargo.toml", str(config.has_cargo_toml))
184
+ table.add_row("Has go.mod", str(config.has_go_mod))
185
+ table.add_row("Has pom.xml", str(config.has_pom_xml))
186
+ table.add_row("Has build.gradle", str(config.has_build_gradle))
187
+ table.add_row("Suggested Target", target)
188
+
189
+ console.print(table)
190
+
191
+
192
+ def main() -> None:
193
+ """Main entry point."""
194
+ app()
195
+
196
+
197
+ if __name__ == "__main__":
198
+ main()
@@ -0,0 +1,300 @@
1
+ """MCP server for OpenAPI code generation and validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import sys
8
+ from collections.abc import Awaitable, Callable
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from mcp.server import Server
13
+ from mcp.types import (
14
+ CallToolRequestParams,
15
+ CallToolResult,
16
+ ListToolsResult,
17
+ PaginatedRequestParams,
18
+ TextContent,
19
+ Tool,
20
+ )
21
+
22
+ from .tools.generate import generate_client, list_generators
23
+ from .tools.validate import validate_spec
24
+ from .types import (
25
+ GeneratorTarget,
26
+ MCPToolError,
27
+ SpecInput,
28
+ SpecSource,
29
+ ValidationTool,
30
+ )
31
+ from .utils.config_inference import infer_project_config
32
+
33
+ logging.basicConfig(
34
+ level=logging.INFO,
35
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
36
+ stream=sys.stderr,
37
+ )
38
+ logger = logging.getLogger(__name__)
39
+
40
+ __all__ = ["main", "create_server"]
41
+
42
+
43
+ _SPEC_SCHEMA = {
44
+ "type": "object",
45
+ "description": "OpenAPI specification input",
46
+ "properties": {
47
+ "source": {
48
+ "type": "string",
49
+ "enum": ["url", "file", "stdin"],
50
+ "description": "Source type for the specification",
51
+ },
52
+ "location": {
53
+ "type": "string",
54
+ "description": "URL, file path, or '-' for stdin",
55
+ },
56
+ "format": {
57
+ "type": "string",
58
+ "enum": ["json", "yaml", "yml"],
59
+ "default": "yaml",
60
+ "description": "Format of the specification",
61
+ },
62
+ },
63
+ "required": ["source", "location"],
64
+ }
65
+
66
+ TOOLS = [
67
+ Tool(
68
+ name="generate-client",
69
+ description=(
70
+ "Generate client code from an OpenAPI specification using openapi-generator "
71
+ "or swagger-codegen"
72
+ ),
73
+ input_schema={
74
+ "type": "object",
75
+ "properties": {
76
+ "spec": _SPEC_SCHEMA,
77
+ "target": {
78
+ "type": "string",
79
+ "enum": [target.value for target in GeneratorTarget],
80
+ "default": "typescript-axios",
81
+ "description": "Target language/framework for generation",
82
+ },
83
+ "output_dir": {
84
+ "type": "string",
85
+ "default": "./generated",
86
+ "description": "Output directory for generated code",
87
+ },
88
+ "package_name": {"type": "string"},
89
+ "package_version": {"type": "string"},
90
+ "additional_properties": {
91
+ "type": "object",
92
+ "additionalProperties": {"type": "string"},
93
+ },
94
+ "generate_tests": {"type": "boolean", "default": True},
95
+ "post_process": {"type": "boolean", "default": True},
96
+ },
97
+ "required": ["spec"],
98
+ },
99
+ ),
100
+ Tool(
101
+ name="validate-spec",
102
+ description="Validate an OpenAPI specification using Spectral or Vacuum",
103
+ input_schema={
104
+ "type": "object",
105
+ "properties": {
106
+ "spec": _SPEC_SCHEMA,
107
+ "tool": {
108
+ "type": "string",
109
+ "enum": ["spectral", "vacuum"],
110
+ "default": "spectral",
111
+ },
112
+ "ruleset": {"type": "string"},
113
+ "fail_on_warnings": {"type": "boolean", "default": False},
114
+ },
115
+ "required": ["spec"],
116
+ },
117
+ ),
118
+ Tool(
119
+ name="list-generators",
120
+ description="List available code generators and their targets",
121
+ input_schema={"type": "object", "properties": {}},
122
+ ),
123
+ Tool(
124
+ name="infer-project",
125
+ description="Infer project configuration and suggest generation target",
126
+ input_schema={"type": "object", "properties": {}},
127
+ ),
128
+ ]
129
+
130
+
131
+ async def _handle_generate_client(arguments: dict[str, Any]) -> CallToolResult:
132
+ spec_data = arguments["spec"]
133
+ spec = SpecInput(
134
+ source=SpecSource(spec_data["source"]),
135
+ location=spec_data["location"],
136
+ format=spec_data.get("format", "yaml"),
137
+ )
138
+ target = GeneratorTarget(arguments.get("target", "typescript-axios"))
139
+ result = await generate_client(
140
+ spec=spec,
141
+ target=target,
142
+ output_dir=Path(arguments.get("output_dir", "./generated")),
143
+ package_name=arguments.get("package_name"),
144
+ package_version=arguments.get("package_version"),
145
+ additional_properties=arguments.get("additional_properties", {}),
146
+ generate_tests=arguments.get("generate_tests", True),
147
+ post_process=arguments.get("post_process", True),
148
+ )
149
+
150
+ if not result.success:
151
+ return CallToolResult(
152
+ content=[TextContent(type="text", text=f"Generation failed: {result.error}")],
153
+ is_error=True,
154
+ )
155
+
156
+ text = f"{result.summary}\nFiles created: {len(result.files_created)}"
157
+ if result.warnings:
158
+ text += "\nWarnings:\n" + "\n".join(f"- {warning}" for warning in result.warnings)
159
+ return CallToolResult(content=[TextContent(type="text", text=text)])
160
+
161
+
162
+ async def _handle_validate_spec(arguments: dict[str, Any]) -> CallToolResult:
163
+ spec_data = arguments["spec"]
164
+ spec = SpecInput(
165
+ source=SpecSource(spec_data["source"]),
166
+ location=spec_data["location"],
167
+ format=spec_data.get("format", "yaml"),
168
+ )
169
+ result = await validate_spec(
170
+ spec=spec,
171
+ tool=ValidationTool(arguments.get("tool", "spectral")),
172
+ ruleset=arguments.get("ruleset"),
173
+ fail_on_warnings=arguments.get("fail_on_warnings", False),
174
+ )
175
+
176
+ if not result.success:
177
+ return CallToolResult(
178
+ content=[TextContent(type="text", text=f"Validation failed: {result.error}")],
179
+ is_error=True,
180
+ )
181
+
182
+ text = "Specification is valid" if result.valid else "Specification invalid"
183
+ text += f"\nErrors: {len(result.errors)}, Warnings: {len(result.warnings)}"
184
+ for label, entries in (("Errors", result.errors), ("Warnings", result.warnings)):
185
+ if entries:
186
+ text += f"\n\n{label}:\n"
187
+ text += "\n".join(
188
+ f"- {item.get('path', '')}: {item.get('message', '')}" for item in entries[:10]
189
+ )
190
+ return CallToolResult(
191
+ content=[TextContent(type="text", text=text)],
192
+ is_error=not result.valid,
193
+ )
194
+
195
+
196
+ async def _handle_list_generators(_arguments: dict[str, Any]) -> CallToolResult:
197
+ result = await list_generators()
198
+ text = f"Generator: {result['generator']}\nAvailable: {'Yes' if result['available'] else 'No'}"
199
+ if result["targets"]:
200
+ text += "\n\nAvailable targets:\n" + "\n".join(
201
+ f"- {target}" for target in sorted(result["targets"])
202
+ )
203
+ else:
204
+ text += "\n\nNo targets found (generator not installed)"
205
+ return CallToolResult(content=[TextContent(type="text", text=text)])
206
+
207
+
208
+ async def _handle_infer_project(_arguments: dict[str, Any]) -> CallToolResult:
209
+ config = infer_project_config()
210
+ text = (
211
+ "Project Configuration:\n"
212
+ f" Language: {config.language}\n"
213
+ f" Framework: {config.framework or 'auto'}\n"
214
+ f" Package Manager: {config.package_manager or 'unknown'}\n"
215
+ f" Has package.json: {config.has_package_json}\n"
216
+ f" Has pyproject.toml: {config.has_pyproject_toml}\n"
217
+ f" Has Cargo.toml: {config.has_cargo_toml}\n"
218
+ f" Has go.mod: {config.has_go_mod}\n"
219
+ f" Has pom.xml: {config.has_pom_xml}\n"
220
+ f" Has build.gradle: {config.has_build_gradle}\n"
221
+ f"\nSuggested Target: {config.primary_language}"
222
+ )
223
+ return CallToolResult(content=[TextContent(type="text", text=text)])
224
+
225
+
226
+ ToolHandler = Callable[[dict[str, Any]], Awaitable[CallToolResult]]
227
+ TOOL_HANDLERS: dict[str, ToolHandler] = {
228
+ "generate-client": _handle_generate_client,
229
+ "validate-spec": _handle_validate_spec,
230
+ "list-generators": _handle_list_generators,
231
+ "infer-project": _handle_infer_project,
232
+ }
233
+
234
+
235
+ async def handle_list_tools(_context: Any, _request: PaginatedRequestParams) -> ListToolsResult:
236
+ return ListToolsResult(tools=TOOLS)
237
+
238
+
239
+ async def handle_call_tool(_context: Any, request: CallToolRequestParams) -> CallToolResult:
240
+ name = request.name
241
+ arguments = request.arguments or {}
242
+ logger.info("Tool called: %s", name)
243
+
244
+ handler = TOOL_HANDLERS.get(name)
245
+ if handler is None:
246
+ return CallToolResult(
247
+ content=[TextContent(type="text", text=f"Unknown tool: {name}")],
248
+ is_error=True,
249
+ )
250
+
251
+ try:
252
+ return await handler(arguments)
253
+ except MCPToolError as error:
254
+ logger.error("MCP tool error: %s", error)
255
+ return CallToolResult(
256
+ content=[TextContent(type="text", text=f"Tool error: {error}")],
257
+ is_error=True,
258
+ )
259
+ except Exception as error:
260
+ logger.exception("Tool execution failed")
261
+ return CallToolResult(
262
+ content=[TextContent(type="text", text=f"Internal error: {error}")],
263
+ is_error=True,
264
+ )
265
+
266
+
267
+ def create_server() -> Server:
268
+ """Create and configure the MCP server."""
269
+ mcp_server = Server("mcp-openapi-generator")
270
+ mcp_server.add_request_handler("tools/list", PaginatedRequestParams, handle_list_tools)
271
+ mcp_server.add_request_handler("tools/call", CallToolRequestParams, handle_call_tool)
272
+ return mcp_server
273
+
274
+
275
+ async def run_server() -> None:
276
+ """Run the MCP server with stdio transport."""
277
+ from mcp.server.stdio import stdio_server
278
+
279
+ mcp_server = create_server()
280
+ async with stdio_server() as (read_stream, write_stream):
281
+ await mcp_server.run(
282
+ read_stream,
283
+ write_stream,
284
+ mcp_server.create_initialization_options(),
285
+ )
286
+
287
+
288
+ def main() -> None:
289
+ """Main entry point for the MCP server."""
290
+ try:
291
+ asyncio.run(run_server())
292
+ except KeyboardInterrupt:
293
+ logger.info("Server stopped")
294
+ except Exception:
295
+ logger.exception("Server error")
296
+ sys.exit(1)
297
+
298
+
299
+ if __name__ == "__main__":
300
+ main()
@@ -0,0 +1,14 @@
1
+ """Tools for the MCP OpenAPI Generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .generate import generate_client, list_generators, GeneratorWrapper
6
+ from .validate import validate_spec, ValidationWrapper
7
+
8
+ __all__ = [
9
+ "generate_client",
10
+ "list_generators",
11
+ "GeneratorWrapper",
12
+ "validate_spec",
13
+ "ValidationWrapper",
14
+ ]