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.
- mcp_openapi_generator/__init__.py +13 -0
- mcp_openapi_generator/cli.py +198 -0
- mcp_openapi_generator/server.py +300 -0
- mcp_openapi_generator/tools/__init__.py +14 -0
- mcp_openapi_generator/tools/generate.py +291 -0
- mcp_openapi_generator/tools/validate.py +277 -0
- mcp_openapi_generator/types/__init__.py +195 -0
- mcp_openapi_generator/utils/__init__.py +21 -0
- mcp_openapi_generator/utils/config_inference.py +219 -0
- mcp_openapi_generator/utils/post_process.py +229 -0
- mcp_openapi_generator/utils/spec_resolver.py +184 -0
- mcp_openapi_generator-1.0.0.dist-info/METADATA +332 -0
- mcp_openapi_generator-1.0.0.dist-info/RECORD +16 -0
- mcp_openapi_generator-1.0.0.dist-info/WHEEL +4 -0
- mcp_openapi_generator-1.0.0.dist-info/entry_points.txt +3 -0
- mcp_openapi_generator-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
]
|