api-to-mcp-forge 0.1.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.
api_to_mcp/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """api-to-mcp: turn API documentation (a docs URL, an OpenAPI/Swagger file, an HTML page, pasted text) into an MCP
2
+ server. The judgement (reading a platform's docs and writing an evidence-only entry) belongs to the model driving the
3
+ skill; the deterministic parts (templates, lint, run config, tests, live checks) are tools here, so any MCP client can
4
+ run them. Entries run with platform-mcp-hub (`platform-mcp-hub serve --entry <file>`)."""
5
+
6
+ __version__ = "0.1.0"
api_to_mcp/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
api_to_mcp/cli.py ADDED
@@ -0,0 +1,167 @@
1
+ """api-to-mcp command line: the same tools the MCP server offers, for a terminal or a script. Every command prints the
2
+ tool's JSON result and exits 1 when the result is an error.
3
+
4
+ api-to-mcp mcp [--http [--port N]] run the MCP server (stdio by default; the Claude Code plugin uses this)
5
+ api-to-mcp doctor [--fix] prerequisites, workspace, install commands
6
+ api-to-mcp workspace where entries are saved
7
+ api-to-mcp search <text> [--category C] existing entries (yours and platform-mcp-hub's)
8
+ api-to-mcp get <category> <id> one entry's JSON
9
+ api-to-mcp template [generic | <category>] skeleton, example and adapter contract (default generic)
10
+ api-to-mcp ingest <url | file | -> [--filter F] summarise an OpenAPI/Swagger/Postman/RAML/WSDL/GraphQL/... description
11
+ api-to-mcp read-docs <url> [--offset N] [--reader] readable text of a documentation page
12
+ api-to-mcp draft <spec url|file> <id> [--operations a,b] [--filter F] [--limit N] [--docs-url URL]
13
+ draft a generic entry from an OpenAPI/Swagger description
14
+ api-to-mcp save <category> <id> <entry.json | -> save an entry into the workspace
15
+ api-to-mcp lint <category> <id>
16
+ api-to-mcp generate <category> <id> run config (or a checkout's registry metadata) + contract test template
17
+ api-to-mcp try <category> <id> <verb> ['<json args>']
18
+ api-to-mcp test <category> <id> lint + contract tests + stdio smoke of both runtimes
19
+ api-to-mcp verify <category> <id> [--lang py|ts|both] [--plan '<json>'] [--record]
20
+ api-to-mcp serve <category> <id> [--http ...] start the entry (platform-mcp-hub serve --entry <file>)
21
+ api-to-mcp call <tool> '<json arguments>' any tool by name
22
+
23
+ Global options (before the command): --home DIR (your workspace; API_TO_MCP_HOME) or --checkout DIR (a platform-mcp
24
+ checkout, to contribute an entry upstream; API_TO_MCP_CHECKOUT).
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import asyncio
30
+ import json
31
+ import os
32
+ import sys
33
+
34
+ from . import __version__
35
+
36
+
37
+ def _pop_flag(argv: list[str], flag: str) -> bool:
38
+ if flag in argv:
39
+ argv.remove(flag)
40
+ return True
41
+ return False
42
+
43
+
44
+ def _pop_value(argv: list[str], flag: str) -> str | None:
45
+ if flag in argv:
46
+ i = argv.index(flag)
47
+ if i + 1 >= len(argv):
48
+ raise SystemExit(f"api-to-mcp: {flag} needs a value")
49
+ v = argv[i + 1]
50
+ del argv[i:i + 2]
51
+ return v
52
+ return None
53
+
54
+
55
+ def _json_arg(text: str) -> object:
56
+ if text == "-":
57
+ text = sys.stdin.read()
58
+ elif os.path.isfile(text):
59
+ with open(text, encoding="utf-8") as f:
60
+ text = f.read()
61
+ try:
62
+ return json.loads(text)
63
+ except ValueError as exc:
64
+ raise SystemExit(f"api-to-mcp: not JSON: {exc}")
65
+
66
+
67
+ def _call(tool: str, args: dict) -> int:
68
+ from . import server
69
+ res = asyncio.run(server.server.call_tool(tool, args))
70
+ payload = res.structured_content if res.structured_content is not None else {"text": res.content[0].text if res.content else ""}
71
+ print(json.dumps(payload, indent=1, ensure_ascii=False, default=str))
72
+ return 1 if res.is_error else 0
73
+
74
+
75
+ def _need(argv: list[str], n: int, usage: str) -> list[str]:
76
+ if len(argv) < n:
77
+ raise SystemExit(f"usage: api-to-mcp {usage}")
78
+ return argv
79
+
80
+
81
+ def main(argv: list[str] | None = None) -> int:
82
+ import logging
83
+ logging.getLogger("httpx").setLevel(logging.WARNING) # one line per request on stderr is noise in a terminal
84
+ argv = list(sys.argv[1:] if argv is None else argv)
85
+ home, checkout = _pop_value(argv, "--home"), _pop_value(argv, "--checkout")
86
+ if home:
87
+ os.environ["API_TO_MCP_HOME"] = home
88
+ if checkout:
89
+ os.environ["API_TO_MCP_CHECKOUT"] = checkout
90
+ if not argv or argv[0] in ("-h", "--help", "help"):
91
+ print(__doc__.strip())
92
+ return 0 if argv else 2
93
+ if argv[0] in ("-V", "--version", "version"):
94
+ print(f"api-to-mcp {__version__}")
95
+ return 0
96
+ cmd, rest = argv[0], argv[1:]
97
+ if cmd == "mcp":
98
+ from . import server
99
+ server.main(rest)
100
+ return 0
101
+ if cmd == "doctor":
102
+ return _call("doctor", {"fix": _pop_flag(rest, "--fix")})
103
+ if cmd == "workspace":
104
+ return _call("workspace", {})
105
+ if cmd == "search":
106
+ category = _pop_value(rest, "--category")
107
+ _need(rest, 1, "search <text> [--category C]")
108
+ return _call("catalog_search", {"query": " ".join(rest), **({"category": category} if category else {})})
109
+ if cmd == "get":
110
+ c, i = _need(rest, 2, "get <category> <id>")[:2]
111
+ return _call("catalog_get", {"category": c, "id": i})
112
+ if cmd == "template":
113
+ return _call("template_entry", {"category": rest[0] if rest else "generic"})
114
+ if cmd == "draft":
115
+ ops = _pop_value(rest, "--operations")
116
+ flt = _pop_value(rest, "--filter")
117
+ lim = _pop_value(rest, "--limit")
118
+ docs = _pop_value(rest, "--docs-url")
119
+ src, pid = _need(rest, 2, "draft <spec url | file> <id> [--operations a,b] [--filter F] [--limit N] [--docs-url URL]")[:2]
120
+ return _call("draft_entry", {"source": src, "id": pid, **({"operations": [o.strip() for o in ops.split(",") if o.strip()]} if ops else {}),
121
+ **({"filter": flt} if flt else {}), **({"limit": int(lim)} if lim else {}), **({"docs_url": docs} if docs else {})})
122
+ if cmd == "ingest":
123
+ flt = _pop_value(rest, "--filter")
124
+ src = _need(rest, 1, "ingest <url | file | -> [--filter F]")[0]
125
+ return _call("ingest_openapi", {"source": sys.stdin.read() if src == "-" else src, **({"filter": flt} if flt else {})})
126
+ if cmd == "read-docs":
127
+ offset = _pop_value(rest, "--offset")
128
+ reader = _pop_flag(rest, "--reader")
129
+ url = _need(rest, 1, "read-docs <url> [--offset N] [--reader]")[0]
130
+ return _call("read_docs", {"url": url, "reader": reader, **({"offset": int(offset)} if offset else {})})
131
+ if cmd == "save":
132
+ c, i, src = _need(rest, 3, "save <category> <id> <entry.json | ->")[:3]
133
+ entry = _json_arg(src)
134
+ if isinstance(entry, dict) and isinstance(entry.get("entry"), dict) and "drafted" in entry:
135
+ entry = entry["entry"] # the output of `api-to-mcp draft`, saved as it is
136
+ return _call("save_entry", {"category": c, "id": i, "entry": entry})
137
+ if cmd in ("lint", "generate", "test"):
138
+ c, i = _need(rest, 2, f"{cmd} <category> <id>")[:2]
139
+ return _call({"lint": "lint_entry", "generate": "generate_server", "test": "test_server"}[cmd], {"category": c, "id": i})
140
+ if cmd == "try":
141
+ c, i, verb = _need(rest, 3, "try <category> <id> <verb> ['<json args>']")[:3]
142
+ return _call("try_tool", {"category": c, "id": i, "verb": verb, "arguments": _json_arg(rest[3]) if len(rest) > 3 else {}})
143
+ if cmd == "verify":
144
+ lang = _pop_value(rest, "--lang") or "both"
145
+ plan = _pop_value(rest, "--plan")
146
+ record = _pop_flag(rest, "--record")
147
+ c, i = _need(rest, 2, "verify <category> <id> [--lang py|ts|both] [--plan JSON] [--record]")[:2]
148
+ return _call("live_verify", {"category": c, "id": i, "lang": lang, "record": record, **({"plan": _json_arg(plan)} if plan else {})})
149
+ if cmd == "serve":
150
+ c, i = _need(rest, 2, "serve <category> <id> [--http ...]")[:2]
151
+ from . import server
152
+ path = server.WS.catalog / c / f"{i}.json"
153
+ if not path.is_file():
154
+ raise SystemExit(f"api-to-mcp: no entry {path}")
155
+ os.execvpe(server.WS.python(), [server.WS.python(), "-m", "platform_mcp_hub", "serve", "--entry", str(path), *rest[2:]], server.WS.gate_env(dict(os.environ)))
156
+ if cmd == "call":
157
+ tool = _need(rest, 1, "call <tool> '<json arguments>'")[0]
158
+ args = _json_arg(rest[1]) if len(rest) > 1 else {}
159
+ if not isinstance(args, dict):
160
+ raise SystemExit("api-to-mcp: the arguments must be a JSON object")
161
+ return _call(tool, args)
162
+ print(f"api-to-mcp: unknown command {cmd!r}; run `api-to-mcp --help`", file=sys.stderr)
163
+ return 2
164
+
165
+
166
+ def entry() -> None:
167
+ sys.exit(main())