openapi-to-mcp-cli 0.9.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 (103) hide show
  1. openapi_to_mcp/__init__.py +3 -0
  2. openapi_to_mcp/adapters/__init__.py +1 -0
  3. openapi_to_mcp/adapters/generator.py +206 -0
  4. openapi_to_mcp/adapters/spec_loader.py +135 -0
  5. openapi_to_mcp/adapters/testing/__init__.py +21 -0
  6. openapi_to_mcp/adapters/testing/models.py +46 -0
  7. openapi_to_mcp/adapters/testing/response_formatting.py +94 -0
  8. openapi_to_mcp/adapters/testing/server_tester.py +85 -0
  9. openapi_to_mcp/adapters/testing/stdio_transport.py +83 -0
  10. openapi_to_mcp/adapters/testing/streamable_http_transport.py +163 -0
  11. openapi_to_mcp/cli.py +45 -0
  12. openapi_to_mcp/commands/__init__.py +15 -0
  13. openapi_to_mcp/commands/diff.py +138 -0
  14. openapi_to_mcp/commands/doctor.py +97 -0
  15. openapi_to_mcp/commands/generate.py +78 -0
  16. openapi_to_mcp/commands/generation_context.py +109 -0
  17. openapi_to_mcp/commands/generation_models.py +163 -0
  18. openapi_to_mcp/commands/generation_service.py +142 -0
  19. openapi_to_mcp/commands/options.py +153 -0
  20. openapi_to_mcp/commands/policy_support.py +22 -0
  21. openapi_to_mcp/commands/run.py +102 -0
  22. openapi_to_mcp/commands/run_support.py +119 -0
  23. openapi_to_mcp/commands/runtime_overrides.py +164 -0
  24. openapi_to_mcp/commands/test_server.py +160 -0
  25. openapi_to_mcp/common/__init__.py +25 -0
  26. openapi_to_mcp/common/decorators.py +50 -0
  27. openapi_to_mcp/common/error_policy.py +12 -0
  28. openapi_to_mcp/common/exceptions.py +34 -0
  29. openapi_to_mcp/common/logger.py +78 -0
  30. openapi_to_mcp/common/performance_presets.py +79 -0
  31. openapi_to_mcp/common/terminal.py +61 -0
  32. openapi_to_mcp/common/tool_runtime.py +81 -0
  33. openapi_to_mcp/common/utils.py +89 -0
  34. openapi_to_mcp/diff/__init__.py +6 -0
  35. openapi_to_mcp/diff/analyzer.py +163 -0
  36. openapi_to_mcp/diff/models.py +70 -0
  37. openapi_to_mcp/diff/surface.py +110 -0
  38. openapi_to_mcp/doctor/__init__.py +6 -0
  39. openapi_to_mcp/doctor/analyzer.py +192 -0
  40. openapi_to_mcp/doctor/models.py +75 -0
  41. openapi_to_mcp/doctor/security.py +69 -0
  42. openapi_to_mcp/mapping/__init__.py +5 -0
  43. openapi_to_mcp/mapping/mapper.py +515 -0
  44. openapi_to_mcp/mapping/output_schema.py +122 -0
  45. openapi_to_mcp/mapping/tool_description.py +113 -0
  46. openapi_to_mcp/mapping/tool_examples.py +145 -0
  47. openapi_to_mcp/mapping/tool_grouping.py +87 -0
  48. openapi_to_mcp/mapping/utils.py +59 -0
  49. openapi_to_mcp/policy/__init__.py +7 -0
  50. openapi_to_mcp/policy/applier.py +208 -0
  51. openapi_to_mcp/policy/loader.py +56 -0
  52. openapi_to_mcp/policy/models.py +77 -0
  53. openapi_to_mcp/policy/parser.py +192 -0
  54. openapi_to_mcp/policy/settings.py +42 -0
  55. openapi_to_mcp/policy/validators.py +111 -0
  56. openapi_to_mcp/schema/__init__.py +5 -0
  57. openapi_to_mcp/schema/converter.py +199 -0
  58. openapi_to_mcp/schema/handlers/__init__.py +17 -0
  59. openapi_to_mcp/schema/handlers/array_schema.py +37 -0
  60. openapi_to_mcp/schema/handlers/base.py +76 -0
  61. openapi_to_mcp/schema/handlers/common.py +55 -0
  62. openapi_to_mcp/schema/handlers/composition.py +41 -0
  63. openapi_to_mcp/schema/handlers/number_schema.py +58 -0
  64. openapi_to_mcp/schema/handlers/object_schema.py +46 -0
  65. openapi_to_mcp/schema/handlers/reference.py +142 -0
  66. openapi_to_mcp/schema/handlers/string_schema.py +63 -0
  67. openapi_to_mcp/templates/.env.example.j2 +63 -0
  68. openapi_to_mcp/templates/README.md.j2 +137 -0
  69. openapi_to_mcp/templates/package.json.j2 +39 -0
  70. openapi_to_mcp/templates/src/custom/tools.ts.j2 +5 -0
  71. openapi_to_mcp/templates/src/index.ts.j2 +21 -0
  72. openapi_to_mcp/templates/src/runtime/audit.ts.j2 +98 -0
  73. openapi_to_mcp/templates/src/runtime/auth.ts.j2 +118 -0
  74. openapi_to_mcp/templates/src/runtime/cache.ts.j2 +100 -0
  75. openapi_to_mcp/templates/src/runtime/circuit_breaker.ts.j2 +153 -0
  76. openapi_to_mcp/templates/src/runtime/config.ts.j2 +169 -0
  77. openapi_to_mcp/templates/src/runtime/errors.ts.j2 +169 -0
  78. openapi_to_mcp/templates/src/runtime/executor.ts.j2 +135 -0
  79. openapi_to_mcp/templates/src/runtime/executor_support.ts.j2 +164 -0
  80. openapi_to_mcp/templates/src/runtime/generated.ts.j2 +60 -0
  81. openapi_to_mcp/templates/src/runtime/http_transport.ts.j2 +78 -0
  82. openapi_to_mcp/templates/src/runtime/limiter.ts.j2 +77 -0
  83. openapi_to_mcp/templates/src/runtime/observability.ts.j2 +24 -0
  84. openapi_to_mcp/templates/src/runtime/performance_preset.ts.j2 +60 -0
  85. openapi_to_mcp/templates/src/runtime/rate_limit.ts.j2 +47 -0
  86. openapi_to_mcp/templates/src/runtime/redaction.ts.j2 +111 -0
  87. openapi_to_mcp/templates/src/runtime/request.ts.j2 +185 -0
  88. openapi_to_mcp/templates/src/runtime/request_context.ts.j2 +48 -0
  89. openapi_to_mcp/templates/src/runtime/resilience.ts.j2 +120 -0
  90. openapi_to_mcp/templates/src/runtime/response.ts.j2 +32 -0
  91. openapi_to_mcp/templates/src/runtime/retry.ts.j2 +61 -0
  92. openapi_to_mcp/templates/src/runtime/serialization.ts.j2 +167 -0
  93. openapi_to_mcp/templates/src/runtime/tool_access.ts.j2 +122 -0
  94. openapi_to_mcp/templates/src/runtime/validation.ts.j2 +53 -0
  95. openapi_to_mcp/templates/src/server.ts.j2 +151 -0
  96. openapi_to_mcp/templates/src/transport_stdio.ts.j2 +18 -0
  97. openapi_to_mcp/templates/src/transport_streamable_http.ts.j2 +169 -0
  98. openapi_to_mcp/templates/tsconfig.json.j2 +21 -0
  99. openapi_to_mcp_cli-0.9.1.dist-info/METADATA +131 -0
  100. openapi_to_mcp_cli-0.9.1.dist-info/RECORD +103 -0
  101. openapi_to_mcp_cli-0.9.1.dist-info/WHEEL +4 -0
  102. openapi_to_mcp_cli-0.9.1.dist-info/entry_points.txt +2 -0
  103. openapi_to_mcp_cli-0.9.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,163 @@
1
+ """Streamable HTTP transport for MCP server tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ from typing import Any
9
+
10
+ import requests
11
+ from mcp.types import CallToolResult, ListToolsResult
12
+
13
+ from openapi_to_mcp.adapters.testing.models import (
14
+ ServerConnectionError,
15
+ TransportStrategy,
16
+ UnsupportedMethodError,
17
+ )
18
+ from openapi_to_mcp.adapters.testing.response_formatting import format_mcp_response
19
+
20
+ logger = logging.getLogger(__name__)
21
+ DEFAULT_PROTOCOL_VERSION = "2025-11-25"
22
+
23
+
24
+ def _normalize_result(
25
+ response: dict[str, Any], method: str, req_id: int
26
+ ) -> dict[str, Any]:
27
+ if "error" in response or not isinstance(response.get("result"), dict):
28
+ return response
29
+ model_type = ListToolsResult if method == "list" else CallToolResult
30
+ model = model_type.model_validate(response["result"])
31
+ formatted = format_mcp_response(model, req_id)
32
+ normalized = dict(response)
33
+ normalized["result"] = {
34
+ key: value for key, value in formatted.items() if key not in {"jsonrpc", "id"}
35
+ }
36
+ return normalized
37
+
38
+
39
+ class StreamableHttpTransport(TransportStrategy):
40
+ """Execute MCP JSON-RPC requests through streamable HTTP."""
41
+
42
+ def __init__(self, endpoint_url: str) -> None:
43
+ if not endpoint_url:
44
+ raise ValueError("endpoint_url is required for streamable-http transport")
45
+ self.endpoint_url = endpoint_url
46
+ self._session_id: str | None = None
47
+ self._protocol_version = DEFAULT_PROTOCOL_VERSION
48
+
49
+ def _build_jsonrpc_payload(
50
+ self, method: str, params: dict[str, Any] | None, req_id: int
51
+ ) -> dict[str, Any]:
52
+ if method == "list":
53
+ return {
54
+ "jsonrpc": "2.0",
55
+ "id": req_id,
56
+ "method": "tools/list",
57
+ "params": {},
58
+ }
59
+ if method == "call":
60
+ if params is None or "tool_name" not in params:
61
+ raise ValueError("Missing 'tool_name' in params for tool call method.")
62
+ return {
63
+ "jsonrpc": "2.0",
64
+ "id": req_id,
65
+ "method": "tools/call",
66
+ "params": {
67
+ "name": params["tool_name"],
68
+ "arguments": params.get("tool_arguments", {}),
69
+ },
70
+ }
71
+ raise UnsupportedMethodError(f"Unsupported method for testing: {method}")
72
+
73
+ def _build_headers(self) -> dict[str, str]:
74
+ headers = {
75
+ "Content-Type": "application/json",
76
+ "Accept": "application/json, text/event-stream",
77
+ "MCP-Protocol-Version": self._protocol_version,
78
+ }
79
+ if self._session_id:
80
+ headers["Mcp-Session-Id"] = self._session_id
81
+ return headers
82
+
83
+ def _parse_json_response(self, response: requests.Response) -> dict[str, Any]:
84
+ try:
85
+ response_data = response.json()
86
+ except json.JSONDecodeError as error:
87
+ raise ServerConnectionError(
88
+ f"Invalid JSON response from streamable-http endpoint: {error}"
89
+ ) from error
90
+ if not isinstance(response_data, dict):
91
+ raise ServerConnectionError(
92
+ "Unexpected non-object JSON response from streamable-http endpoint"
93
+ )
94
+ return response_data
95
+
96
+ def _initialize(self, req_id: int) -> None:
97
+ payload = {
98
+ "jsonrpc": "2.0",
99
+ "id": req_id,
100
+ "method": "initialize",
101
+ "params": {
102
+ "protocolVersion": self._protocol_version,
103
+ "capabilities": {},
104
+ "clientInfo": {
105
+ "name": "openapi-to-mcp-tester",
106
+ "version": "0.1.0",
107
+ },
108
+ },
109
+ }
110
+ response = requests.post(
111
+ self.endpoint_url,
112
+ json=payload,
113
+ timeout=30,
114
+ headers=self._build_headers(),
115
+ )
116
+ response.raise_for_status()
117
+ self._session_id = response.headers.get("Mcp-Session-Id")
118
+ response_data = self._parse_json_response(response)
119
+ if "error" in response_data:
120
+ raise ServerConnectionError(
121
+ f"Initialize request failed: {response_data['error']}"
122
+ )
123
+ self._send_initialized_notification()
124
+
125
+ def _send_initialized_notification(self) -> None:
126
+ response = requests.post(
127
+ self.endpoint_url,
128
+ json={
129
+ "jsonrpc": "2.0",
130
+ "method": "notifications/initialized",
131
+ "params": {},
132
+ },
133
+ timeout=30,
134
+ headers=self._build_headers(),
135
+ )
136
+ response.raise_for_status()
137
+
138
+ def _post_jsonrpc(
139
+ self, method: str, params: dict[str, Any] | None, req_id: int
140
+ ) -> dict[str, Any]:
141
+ self._initialize(req_id=req_id * 1000 + 1)
142
+ response = requests.post(
143
+ self.endpoint_url,
144
+ json=self._build_jsonrpc_payload(method, params, req_id),
145
+ timeout=30,
146
+ headers=self._build_headers(),
147
+ )
148
+ response.raise_for_status()
149
+ response_data = self._parse_json_response(response)
150
+ return _normalize_result(response_data, method, req_id)
151
+
152
+ async def connect_and_execute(
153
+ self, method: str, params: dict[str, Any] | None, req_id: int
154
+ ) -> dict[str, Any]:
155
+ """Connect over streamable HTTP and return the JSON-RPC response."""
156
+ logger.info("Connecting to streamable-http endpoint at %s", self.endpoint_url)
157
+ try:
158
+ return await asyncio.to_thread(self._post_jsonrpc, method, params, req_id)
159
+ except Exception as error:
160
+ logger.exception("Error during streamable-http connection")
161
+ raise ServerConnectionError(
162
+ f"Failed to connect via streamable-http: {error}"
163
+ ) from error
openapi_to_mcp/cli.py ADDED
@@ -0,0 +1,45 @@
1
+ import rich_click as click
2
+
3
+ from openapi_to_mcp.commands import diff, doctor, generate, run, test_server
4
+ from openapi_to_mcp.common import configure_logger
5
+
6
+
7
+ def _configure_rich_click() -> None:
8
+ click.rich_click.TEXT_MARKUP = "rich"
9
+ click.rich_click.SHOW_ARGUMENTS = True
10
+ click.rich_click.GROUP_ARGUMENTS_OPTIONS = True
11
+ click.rich_click.OPTIONS_TABLE_HELP_SECTIONS = [
12
+ "help",
13
+ "metavar",
14
+ "envvar",
15
+ "default",
16
+ "required",
17
+ ]
18
+ click.rich_click.STYLE_COMMAND = "bold yellow"
19
+ click.rich_click.STYLE_OPTION = "bold cyan"
20
+ click.rich_click.STYLE_SWITCH = "bold cyan"
21
+ click.rich_click.STYLE_METAVAR = "bold white"
22
+ click.rich_click.STYLE_OPTION_HELP = "white"
23
+ click.rich_click.STYLE_HEADER_TEXT = "bold white"
24
+ click.rich_click.STYLE_USAGE = "bold green"
25
+ click.rich_click.STYLE_ERRORS_SUGGESTION = "bold red"
26
+
27
+
28
+ _configure_rich_click()
29
+
30
+
31
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
32
+ def cli() -> None:
33
+ """A CLI tool to diagnose, diff, generate, run, and test MCP servers from OpenAPI specs."""
34
+ configure_logger()
35
+
36
+
37
+ cli.add_command(generate)
38
+ cli.add_command(run)
39
+ cli.add_command(test_server)
40
+ cli.add_command(doctor)
41
+ cli.add_command(diff)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ cli()
@@ -0,0 +1,15 @@
1
+ """Commands component for the openapi-to-mcp package."""
2
+
3
+ from openapi_to_mcp.commands.diff import diff
4
+ from openapi_to_mcp.commands.doctor import doctor
5
+ from openapi_to_mcp.commands.generate import generate
6
+ from openapi_to_mcp.commands.run import run_server as run
7
+ from openapi_to_mcp.commands.test_server import run_test_server as test_server
8
+
9
+ __all__ = [
10
+ "diff",
11
+ "doctor",
12
+ "generate",
13
+ "run",
14
+ "test_server",
15
+ ]
@@ -0,0 +1,138 @@
1
+ """CLI command for diffing MCP-surface changes between two specs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING, Literal
7
+
8
+ import rich_click as click
9
+
10
+ from openapi_to_mcp.adapters.spec_loader import SpecLoader
11
+ from openapi_to_mcp.common import MappingError
12
+ from openapi_to_mcp.common.exceptions import SpecLoaderError
13
+ from openapi_to_mcp.common.terminal import (
14
+ print_error_panel,
15
+ print_section,
16
+ print_success_panel,
17
+ print_warning_panel,
18
+ )
19
+ from openapi_to_mcp.diff import DiffAnalyzer, DiffReport
20
+
21
+ OutputFormat = Literal["json", "text"]
22
+
23
+ if TYPE_CHECKING:
24
+ from openapi_to_mcp.diff.models import FailMode
25
+
26
+
27
+ @click.command()
28
+ @click.option(
29
+ "--before-openapi-json",
30
+ "-b",
31
+ required=True,
32
+ help="Old OpenAPI specification JSON or YAML path or URL.",
33
+ )
34
+ @click.option(
35
+ "--after-openapi-json",
36
+ "-a",
37
+ required=True,
38
+ help="New OpenAPI specification JSON or YAML path or URL.",
39
+ )
40
+ @click.option(
41
+ "--format",
42
+ "output_format",
43
+ type=click.Choice(["text", "json"], case_sensitive=False),
44
+ default="text",
45
+ show_default=True,
46
+ help="Output format for the diff report.",
47
+ )
48
+ @click.option(
49
+ "--fail-on",
50
+ type=click.Choice(["none", "breaking"], case_sensitive=False),
51
+ default="none",
52
+ show_default=True,
53
+ help="Exit non-zero when the selected change class is present.",
54
+ )
55
+ def diff(
56
+ before_openapi_json: str,
57
+ after_openapi_json: str,
58
+ output_format: OutputFormat,
59
+ fail_on: FailMode,
60
+ ) -> None:
61
+ """Compare two OpenAPI specs as generated MCP tool surfaces."""
62
+ report = _build_report(before_openapi_json, after_openapi_json)
63
+ _render_report(report, output_format, fail_on)
64
+ raise click.exceptions.Exit(report.exit_code(fail_on))
65
+
66
+
67
+ def _build_report(before_source: str, after_source: str) -> DiffReport:
68
+ before_spec = _load_spec(before_source)
69
+ after_spec = _load_spec(after_source)
70
+ try:
71
+ return DiffAnalyzer(before_spec, after_spec).analyze(
72
+ before_source, after_source
73
+ )
74
+ except MappingError as exc:
75
+ err_msg = (
76
+ f"Unable to diff MCP surface: {exc}. "
77
+ "Run `openapi-to-mcp doctor` on both specs first."
78
+ )
79
+ raise click.ClickException(err_msg) from exc
80
+
81
+
82
+ def _load_spec(source: str) -> dict:
83
+ try:
84
+ return SpecLoader(source=source).load_and_validate()
85
+ except SpecLoaderError as exc:
86
+ raise click.ClickException(f"Failed to load spec `{source}`: {exc}") from exc
87
+
88
+
89
+ def _render_report(
90
+ report: DiffReport, output_format: OutputFormat, fail_on: FailMode
91
+ ) -> None:
92
+ if output_format == "json":
93
+ click.echo(json.dumps(report.to_dict(fail_on), indent=2))
94
+ return
95
+ if not report.changes:
96
+ print_success_panel(
97
+ "Diff Report",
98
+ [
99
+ f"Before: {report.before_source}",
100
+ f"After: {report.after_source}",
101
+ "No MCP-surface changes detected.",
102
+ f"Exit code: {report.exit_code(fail_on)}",
103
+ ],
104
+ )
105
+ return
106
+ _print_summary(report, fail_on)
107
+ for change in report.changes:
108
+ _render_change(change.to_dict())
109
+
110
+
111
+ def _print_summary(report: DiffReport, fail_on: FailMode) -> None:
112
+ lines = [
113
+ f"Before: {report.before_source}",
114
+ f"After: {report.after_source}",
115
+ f"Breaking: {report.breaking_count()}",
116
+ f"Non-breaking: {report.non_breaking_count()}",
117
+ f"Exit code: {report.exit_code(fail_on)}",
118
+ ]
119
+ if report.breaking_count() > 0:
120
+ print_error_panel("Diff Summary", lines)
121
+ return
122
+ print_warning_panel("Diff Summary", lines)
123
+
124
+
125
+ def _render_change(change: dict[str, object]) -> None:
126
+ print_section(f"{_format_impact(change['impact'])}: {change['code']}")
127
+ click.echo(f"Location: {change['location']}")
128
+ click.echo(f"Issue: {change['message']}")
129
+ details = change.get("details")
130
+ if isinstance(details, dict) and details:
131
+ click.echo(f"Details: {json.dumps(details, sort_keys=True)}")
132
+ click.echo(f"Hint: {change['hint']}")
133
+ click.echo()
134
+
135
+
136
+ def _format_impact(impact: object) -> str:
137
+ value = str(impact).replace("_", "-")
138
+ return value.upper()
@@ -0,0 +1,97 @@
1
+ """Doctor command for spec-readiness diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Literal
7
+
8
+ import rich_click as click
9
+
10
+ from openapi_to_mcp.adapters.spec_loader import SpecLoader
11
+ from openapi_to_mcp.common.exceptions import SpecLoaderError
12
+ from openapi_to_mcp.common.terminal import (
13
+ print_error_panel,
14
+ print_section,
15
+ print_success_panel,
16
+ print_warning_panel,
17
+ )
18
+ from openapi_to_mcp.doctor import DoctorAnalyzer, DoctorReport
19
+
20
+ OutputFormat = Literal["json", "text"]
21
+
22
+
23
+ @click.command()
24
+ @click.option(
25
+ "--openapi-json",
26
+ "-o",
27
+ required=True,
28
+ help="Path or URL to OpenAPI specification JSON or YAML file.",
29
+ )
30
+ @click.option(
31
+ "--format",
32
+ "output_format",
33
+ type=click.Choice(["text", "json"], case_sensitive=False),
34
+ default="text",
35
+ show_default=True,
36
+ help="Output format for diagnostics.",
37
+ )
38
+ def doctor(openapi_json: str, output_format: OutputFormat) -> None:
39
+ """Inspect an OpenAPI spec for MCP-generation readiness."""
40
+ report = _build_report(openapi_json)
41
+ _render_report(report, output_format)
42
+ raise click.exceptions.Exit(report.exit_code())
43
+
44
+
45
+ def _build_report(source: str) -> DoctorReport:
46
+ try:
47
+ spec = SpecLoader(source=source).load_and_validate()
48
+ except SpecLoaderError as exc:
49
+ report = DoctorReport(source=source)
50
+ report.add_error(
51
+ "spec_load_failed",
52
+ str(exc),
53
+ source,
54
+ "Fix the spec source or validation error before generating an MCP server.",
55
+ )
56
+ return report
57
+ return DoctorAnalyzer(spec).analyze(source)
58
+
59
+
60
+ def _render_report(report: DoctorReport, output_format: OutputFormat) -> None:
61
+ if output_format == "json":
62
+ click.echo(json.dumps(report.to_dict(), indent=2))
63
+ return
64
+ if not report.issues:
65
+ print_success_panel(
66
+ "Doctor Report",
67
+ [
68
+ f"Source: {report.source}",
69
+ "No readiness issues found.",
70
+ "Exit code: 0",
71
+ ],
72
+ )
73
+ return
74
+ _print_summary_panel(report)
75
+ for issue in report.issues:
76
+ _render_issue(issue.to_dict())
77
+
78
+
79
+ def _render_issue(issue: dict[str, str]) -> None:
80
+ print_section(f"{issue['severity'].upper()}: {issue['code']}")
81
+ click.echo(f"Location: {issue['location']}")
82
+ click.echo(f"Issue: {issue['message']}")
83
+ click.echo(f"Hint: {issue['hint']}")
84
+ click.echo()
85
+
86
+
87
+ def _print_summary_panel(report: DoctorReport) -> None:
88
+ lines = [
89
+ f"Source: {report.source}",
90
+ f"Errors: {report.error_count()}",
91
+ f"Warnings: {report.warning_count()}",
92
+ f"Exit code: {report.exit_code()}",
93
+ ]
94
+ if report.error_count() > 0:
95
+ print_error_panel("Doctor Summary", lines)
96
+ return
97
+ print_warning_panel("Doctor Summary", lines)
@@ -0,0 +1,78 @@
1
+ """CLI adapter for MCP project generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from typing import Unpack
8
+
9
+ import rich_click as click
10
+
11
+ from openapi_to_mcp.commands.generation_models import (
12
+ GenerateClickOptions,
13
+ GenerationRequest,
14
+ compose_generation_request,
15
+ generation_cli_values,
16
+ )
17
+ from openapi_to_mcp.commands.generation_service import generate_project
18
+ from openapi_to_mcp.commands.options import add_options, generate_options
19
+ from openapi_to_mcp.commands.policy_support import load_policy_and_settings
20
+ from openapi_to_mcp.common.exceptions import (
21
+ GenerationError,
22
+ MappingError,
23
+ NoToolsMappedError,
24
+ PolicyConfigError,
25
+ SchemaError,
26
+ SpecLoaderError,
27
+ )
28
+ from openapi_to_mcp.common.terminal import print_success_panel
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def _generation_request(options: GenerateClickOptions) -> GenerationRequest:
34
+ policy, values = load_policy_and_settings(
35
+ generation_cli_values(options), options["config"]
36
+ )
37
+ return compose_generation_request(
38
+ options["openapi_json"], options["output_dir"], values, policy
39
+ )
40
+
41
+
42
+ def _print_success(output_dir: str) -> None:
43
+ print_success_panel(
44
+ "MCP Server Generation Successful",
45
+ [
46
+ f"Files generated in: {output_dir}",
47
+ "Check the generated README for build and runtime instructions.",
48
+ ],
49
+ )
50
+
51
+
52
+ @click.command()
53
+ @add_options(generate_options)
54
+ def generate(**options: Unpack[GenerateClickOptions]) -> None:
55
+ """Generate a Node.js/TypeScript MCP server from an OpenAPI specification."""
56
+ try:
57
+ generate_project(request=_generation_request(options))
58
+ logger.info("MCP server generation successful.")
59
+ _print_success(options["output_dir"])
60
+ except NoToolsMappedError as error:
61
+ if error.is_error:
62
+ raise click.ClickException(str(error)) from error
63
+ click.echo(str(error))
64
+ except click.ClickException:
65
+ raise
66
+ except (
67
+ SpecLoaderError,
68
+ MappingError,
69
+ GenerationError,
70
+ PolicyConfigError,
71
+ SchemaError,
72
+ ) as error:
73
+ raise click.ClickException(str(error)) from error
74
+ except Exception as error:
75
+ logger.critical(
76
+ "An unexpected critical error occurred: %s", error, exc_info=True
77
+ )
78
+ sys.exit(1)
@@ -0,0 +1,109 @@
1
+ """Build template context values for generated MCP servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from openapi_to_mcp.common.performance_presets import performance_preset_context
9
+
10
+ if TYPE_CHECKING:
11
+ from openapi_to_mcp.commands.generation_models import GenerationSettings
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def _server_name(provided: str | None, spec_info: dict[str, Any]) -> str:
17
+ if provided:
18
+ return provided
19
+ title = spec_info.get("title")
20
+ if title:
21
+ logger.info("Using server name from OpenAPI spec info.title: %s", title)
22
+ return str(title)
23
+ default = "openapi-mcp-server"
24
+ logger.warning(
25
+ "Server name not provided and not found in spec title. Using default: %s",
26
+ default,
27
+ )
28
+ return default
29
+
30
+
31
+ def _server_version(provided: str | None, spec_info: dict[str, Any]) -> str:
32
+ if provided:
33
+ return provided
34
+ version = spec_info.get("version")
35
+ if version:
36
+ logger.info("Using server version from OpenAPI spec info.version: %s", version)
37
+ return str(version)
38
+ default = "1.0.0"
39
+ logger.warning(
40
+ "Server version not provided and not found in spec version. Using default: %s",
41
+ default,
42
+ )
43
+ return default
44
+
45
+
46
+ def _swagger_base_url(spec: dict[str, Any]) -> str | None:
47
+ host = spec.get("host")
48
+ if not isinstance(host, str) or not host:
49
+ return None
50
+ base_path = spec.get("basePath", "")
51
+ schemes = spec.get("schemes", [])
52
+ scheme = schemes[0] if isinstance(schemes, list) and schemes else "https"
53
+ if not isinstance(base_path, str):
54
+ base_path = ""
55
+ if not isinstance(scheme, str) or not scheme:
56
+ return None
57
+ url = f"{scheme}://{host}{base_path}"
58
+ logger.info("Using base URL from Swagger 2 host/basePath: %s", url)
59
+ return url
60
+
61
+
62
+ def _base_url(spec: dict[str, Any]) -> str:
63
+ servers = spec.get("servers", [])
64
+ if isinstance(servers, list) and servers:
65
+ first_server = servers[0]
66
+ if isinstance(first_server, dict) and isinstance(first_server.get("url"), str):
67
+ url = first_server["url"]
68
+ logger.info("Using base URL from spec servers[0].url: %s", url)
69
+ return url
70
+ logger.warning(
71
+ "First server object in spec lacks a valid 'url' string. Using placeholder for .env."
72
+ )
73
+ swagger_url = _swagger_base_url(spec)
74
+ if swagger_url is not None:
75
+ return swagger_url
76
+ logger.warning(
77
+ "No 'servers' array found and no Swagger 2 host/basePath detected. Using placeholder for .env."
78
+ )
79
+ return "YOUR_API_BASE_URL_HERE"
80
+
81
+
82
+ def build_template_context(
83
+ spec: dict[str, Any],
84
+ settings: GenerationSettings,
85
+ public_tools: list[dict[str, Any]],
86
+ runtime_tools: dict[str, dict[str, Any]],
87
+ auth_env_vars: list[str],
88
+ ) -> dict[str, Any]:
89
+ """Build the Jinja context consumed by the project generator."""
90
+ spec_info = spec.get("info", {})
91
+ identity = settings.identity
92
+ transport = settings.transport
93
+ behavior = settings.behavior
94
+ return {
95
+ "server_name": _server_name(identity.name, spec_info),
96
+ "server_version": _server_version(identity.version, spec_info),
97
+ "server_description": spec_info.get("description", ""),
98
+ "transport": transport.kind,
99
+ "host": transport.host,
100
+ "port": transport.port,
101
+ "mcp_endpoint": transport.endpoint,
102
+ "strict": behavior.strict,
103
+ "runtime_validation": behavior.runtime_validation,
104
+ "tools": public_tools,
105
+ "runtime_tools": runtime_tools,
106
+ "auth_env_vars": auth_env_vars,
107
+ "api_base_url_comment": _base_url(spec),
108
+ "performance_presets": performance_preset_context(),
109
+ }