mrpd 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.
Files changed (51) hide show
  1. mrpd/__init__.py +1 -0
  2. mrpd/adapters/__init__.py +1 -0
  3. mrpd/api/app.py +8 -0
  4. mrpd/api/routes.py +200 -0
  5. mrpd/cli.py +182 -0
  6. mrpd/commands/__init__.py +1 -0
  7. mrpd/commands/bridge_mcp.py +189 -0
  8. mrpd/commands/bridge_openapi.py +279 -0
  9. mrpd/commands/init_provider.py +176 -0
  10. mrpd/commands/mrpify_mcp.py +80 -0
  11. mrpd/commands/mrpify_openapi.py +80 -0
  12. mrpd/commands/publish.py +102 -0
  13. mrpd/commands/route.py +126 -0
  14. mrpd/commands/run.py +169 -0
  15. mrpd/commands/serve.py +13 -0
  16. mrpd/commands/validate.py +71 -0
  17. mrpd/core/__init__.py +1 -0
  18. mrpd/core/artifacts.py +39 -0
  19. mrpd/core/config.py +26 -0
  20. mrpd/core/defaults.py +7 -0
  21. mrpd/core/envelopes.py +29 -0
  22. mrpd/core/errors.py +37 -0
  23. mrpd/core/evidence.py +17 -0
  24. mrpd/core/models.py +37 -0
  25. mrpd/core/provider.py +100 -0
  26. mrpd/core/registry.py +115 -0
  27. mrpd/core/schema.py +64 -0
  28. mrpd/core/scoring.py +92 -0
  29. mrpd/core/util.py +27 -0
  30. mrpd/spec/__init__.py +1 -0
  31. mrpd/spec/fixtures/README.md +8 -0
  32. mrpd/spec/fixtures/invalid/bad_msg_type.json +8 -0
  33. mrpd/spec/fixtures/invalid/missing_required_fields.json +7 -0
  34. mrpd/spec/fixtures/valid/discover.json +13 -0
  35. mrpd/spec/fixtures/valid/error.json +13 -0
  36. mrpd/spec/fixtures/valid/offer.json +20 -0
  37. mrpd/spec/schemas/envelope.schema.json +102 -0
  38. mrpd/spec/schemas/manifest.schema.json +52 -0
  39. mrpd/spec/schemas/payloads/discover.schema.json +23 -0
  40. mrpd/spec/schemas/payloads/error.schema.json +15 -0
  41. mrpd/spec/schemas/payloads/evidence.schema.json +17 -0
  42. mrpd/spec/schemas/payloads/execute.schema.json +14 -0
  43. mrpd/spec/schemas/payloads/negotiate.schema.json +15 -0
  44. mrpd/spec/schemas/payloads/offer.schema.json +30 -0
  45. mrpd/spec/schemas/types/artifact.schema.json +15 -0
  46. mrpd/spec/schemas/types/input.schema.json +20 -0
  47. mrpd-0.1.0.dist-info/METADATA +104 -0
  48. mrpd-0.1.0.dist-info/RECORD +51 -0
  49. mrpd-0.1.0.dist-info/WHEEL +5 -0
  50. mrpd-0.1.0.dist-info/entry_points.txt +2 -0
  51. mrpd-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,279 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ import yaml
8
+
9
+
10
+ def _load_spec(path_or_url: str) -> dict:
11
+ # v0: local file only (URL support later)
12
+ p = Path(path_or_url)
13
+ raw = p.read_text(encoding="utf-8")
14
+ if p.suffix.lower() in (".yaml", ".yml"):
15
+ return yaml.safe_load(raw)
16
+ return json.loads(raw)
17
+
18
+
19
+ def bridge_openapi(
20
+ spec: str,
21
+ out_dir: str,
22
+ provider_id: str,
23
+ backend_base_url: str | None,
24
+ capability_prefix: str | None = None,
25
+ ) -> None:
26
+ """Generate an MRP provider wrapper from an OpenAPI spec.
27
+
28
+ v0 behavior:
29
+ - one MRP capability per OpenAPI operationId (optionally prefixed)
30
+ - generates per-operation manifests served at /mrp/manifest/<operationId>
31
+ - execute expects a json input containing optional {path_params, query, body, headers}
32
+ """
33
+
34
+ spec_obj = _load_spec(spec)
35
+ paths = spec_obj.get("paths") or {}
36
+
37
+ servers = spec_obj.get("servers") or []
38
+ default_server = servers[0].get("url") if servers and isinstance(servers[0], dict) else None
39
+ base_url = (backend_base_url or default_server or "").rstrip("/")
40
+
41
+ out = Path(out_dir).resolve()
42
+ out.mkdir(parents=True, exist_ok=True)
43
+ (out / "mrp_manifests").mkdir(exist_ok=True)
44
+
45
+ operations: list[dict] = []
46
+
47
+ for pth, methods in paths.items():
48
+ if not isinstance(methods, dict):
49
+ continue
50
+ for method, op in methods.items():
51
+ if method.lower() not in ("get", "post", "put", "patch", "delete", "head", "options"):
52
+ continue
53
+ if not isinstance(op, dict):
54
+ continue
55
+ op_id = op.get("operationId")
56
+ if not op_id or not isinstance(op_id, str):
57
+ continue
58
+
59
+ cap = f"{capability_prefix}{op_id}" if capability_prefix else op_id
60
+ route_id = f"route:openapi/{cap}@0.1"
61
+
62
+ manifest = {
63
+ "capability_id": f"capability:openapi/{cap}",
64
+ "capability": cap,
65
+ "version": "0.1",
66
+ "tags": ["mrp", "openapi"],
67
+ "inputs": [{"type": "json"}],
68
+ "outputs": [{"type": "json"}],
69
+ "constraints": {"policy": []},
70
+ "proofs_required": [],
71
+ "endpoints": {
72
+ "discover": "/mrp/discover",
73
+ "execute": "/mrp/execute",
74
+ },
75
+ "metadata": {
76
+ "openapi": {
77
+ "method": method.upper(),
78
+ "path": pth,
79
+ "operationId": op_id,
80
+ "capability": cap,
81
+ "base_url": base_url,
82
+ "route_id": route_id,
83
+ }
84
+ },
85
+ }
86
+
87
+ (out / "mrp_manifests" / f"{cap}.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
88
+
89
+ operations.append({"operationId": op_id, "capability": cap, "method": method.upper(), "path": pth, "route_id": route_id})
90
+
91
+ if not operations:
92
+ raise typer.Exit(code=2)
93
+
94
+ # Wrapper server
95
+ app_py = '''from __future__ import annotations
96
+
97
+ import json
98
+ from pathlib import Path
99
+ from urllib.parse import urlencode
100
+
101
+ import httpx
102
+ from fastapi import FastAPI
103
+
104
+ APP_DIR = Path(__file__).resolve().parent
105
+ MANIFEST_DIR = APP_DIR / "mrp_manifests"
106
+
107
+ # Configure backend base URL here (or via env in a real deployment)
108
+ BACKEND_BASE_URL = ("''' + base_url + '''").rstrip("/")
109
+
110
+ app = FastAPI(title="MRP OpenAPI Bridge")
111
+
112
+
113
+ def _load_manifest(capability: str) -> dict:
114
+ fp = MANIFEST_DIR / f"{capability}.json"
115
+ return json.loads(fp.read_text(encoding="utf-8"))
116
+
117
+
118
+ def _capability_from_route_id(route_id: str) -> str:
119
+ # route:openapi/<capability>@0.1
120
+ if not route_id.startswith("route:openapi/"):
121
+ raise ValueError("invalid route_id")
122
+ rest = route_id[len("route:openapi/"):]
123
+ cap = rest.split("@", 1)[0]
124
+ return cap
125
+
126
+
127
+ @app.get("/.well-known/mrp.json")
128
+ def well_known() -> dict:
129
+ # NOTE: This lists multiple capabilities; manifests are per-capability.
130
+ caps = []
131
+ for fp in sorted(MANIFEST_DIR.glob("*.json")):
132
+ try:
133
+ m = json.loads(fp.read_text(encoding="utf-8"))
134
+ c = m.get("capability")
135
+ if isinstance(c, str):
136
+ caps.append(c)
137
+ except Exception:
138
+ continue
139
+
140
+ return {
141
+ "mrp_version": "0.1",
142
+ "id": "''' + provider_id + '''",
143
+ "name": "MRP OpenAPI Bridge",
144
+ "manifest_url": "/mrp/manifest/" + (caps[0] if caps else ""),
145
+ "capabilities": caps,
146
+ }
147
+
148
+
149
+ @app.get("/mrp/manifest/{capability}")
150
+ def mrp_manifest(capability: str) -> dict:
151
+ return _load_manifest(capability)
152
+
153
+
154
+ @app.post("/mrp/discover")
155
+ def mrp_discover(envelope: dict) -> dict:
156
+ req_id = envelope.get("msg_id")
157
+ sender = (envelope.get("sender") or {}).get("id")
158
+ constraints = (envelope.get("payload") or {}).get("constraints") or {}
159
+
160
+ wanted = constraints.get("capability")
161
+
162
+ offers = []
163
+ for fp in sorted(MANIFEST_DIR.glob("*.json")):
164
+ m = json.loads(fp.read_text(encoding="utf-8"))
165
+ cap = m.get("capability")
166
+ meta = (m.get("metadata") or {}).get("openapi") or {}
167
+ route_id = meta.get("route_id")
168
+ if wanted and cap != wanted:
169
+ continue
170
+ offers.append({
171
+ "route_id": route_id,
172
+ "capability": cap,
173
+ "constraints": m.get("constraints") or {},
174
+ "cost": {"unit": "usd", "estimate": 0},
175
+ "latency_ms": 0,
176
+ "proofs": [],
177
+ })
178
+
179
+ return {
180
+ "mrp_version": "0.1",
181
+ "msg_id": str(__import__("uuid").uuid4()),
182
+ "msg_type": "OFFER",
183
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
184
+ "sender": {"id": "''' + provider_id + '''"},
185
+ "receiver": {"id": sender} if sender else None,
186
+ "in_reply_to": req_id,
187
+ "payload": {"offers": offers[:25]},
188
+ }
189
+
190
+
191
+ @app.post("/mrp/execute")
192
+ def mrp_execute(envelope: dict) -> dict:
193
+ req_id = envelope.get("msg_id")
194
+ sender = (envelope.get("sender") or {}).get("id")
195
+
196
+ payload = envelope.get("payload") or {}
197
+ route_id = payload.get("route_id")
198
+ capability = _capability_from_route_id(route_id)
199
+
200
+ m = _load_manifest(capability)
201
+ meta = (m.get("metadata") or {}).get("openapi") or {}
202
+
203
+ method = meta.get("method")
204
+ pth = meta.get("path")
205
+
206
+ if not BACKEND_BASE_URL:
207
+ raise RuntimeError("BACKEND_BASE_URL is not configured")
208
+
209
+ # Convention: json input value may include {path_params, query, body, headers}
210
+ inputs = payload.get("inputs") or []
211
+ inp = next((i for i in inputs if isinstance(i, dict) and i.get("type") == "json"), None)
212
+ spec = inp.get("value") if isinstance(inp, dict) else None
213
+ spec = spec if isinstance(spec, dict) else {}
214
+
215
+ path_params = spec.get("path_params") or {}
216
+ query = spec.get("query") or {}
217
+ body = spec.get("body")
218
+ headers = spec.get("headers") or {}
219
+
220
+ url_path = pth
221
+ for k, v in path_params.items():
222
+ url_path = url_path.replace("{" + str(k) + "}", str(v))
223
+
224
+ url = BACKEND_BASE_URL + url_path
225
+ if query:
226
+ url = url + "?" + urlencode(query, doseq=True)
227
+
228
+ with httpx.Client(timeout=60.0, follow_redirects=False) as client:
229
+ r = client.request(method, url, json=body, headers=headers)
230
+ data = None
231
+ try:
232
+ data = r.json()
233
+ except Exception:
234
+ data = {"status_code": r.status_code, "text": r.text}
235
+
236
+ return {
237
+ "mrp_version": "0.1",
238
+ "msg_id": str(__import__("uuid").uuid4()),
239
+ "msg_type": "EVIDENCE",
240
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
241
+ "sender": {"id": "''' + provider_id + '''"},
242
+ "receiver": {"id": sender} if sender else None,
243
+ "in_reply_to": req_id,
244
+ "payload": {
245
+ "route_id": route_id,
246
+ "outputs": [{"type": "json", "value": data}],
247
+ "provenance": {"citations": [url], "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z"},
248
+ "usage": {"tokens_in_est": 0, "tokens_out_est": 0},
249
+ "job_id": (payload.get("job") or {}).get("id"),
250
+ },
251
+ }
252
+ '''
253
+
254
+ (out / "app.py").write_text(app_py, encoding="utf-8")
255
+
256
+ (out / "README.md").write_text(
257
+ "# MRP OpenAPI Bridge\n\n"
258
+ "Generated by `mrpd bridge openapi`.\n\n"
259
+ "## Run locally\n\n"
260
+ "```bash\n"
261
+ "python -m venv .venv\n"
262
+ "pip install fastapi uvicorn httpx pyyaml\n"
263
+ "uvicorn app:app --host 127.0.0.1 --port 8787 --reload\n"
264
+ "```\n\n"
265
+ "## Publish\n\n"
266
+ "Each OpenAPI operationId becomes a capability. After deploying, publish each manifest you want discoverable:\n\n"
267
+ "Example:\n\n"
268
+ "```bash\n"
269
+ "mrpd publish --manifest-url https://YOUR_DOMAIN/mrp/manifest/<operationId>\n"
270
+ "```\n",
271
+ encoding="utf-8",
272
+ )
273
+
274
+ typer.echo(f"Generated OpenAPI bridge in: {out}")
275
+ typer.echo(f"Operations: {len(operations)}")
276
+ if base_url:
277
+ typer.echo(f"Backend base URL: {base_url}")
278
+ else:
279
+ typer.echo("Backend base URL: (unset) - edit app.py BACKEND_BASE_URL")
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+
10
+ def init_provider(
11
+ out_dir: str,
12
+ capability: str,
13
+ provider_id: str,
14
+ name: str,
15
+ description: str,
16
+ policy: list[str] | None,
17
+ ) -> None:
18
+ """Scaffold a minimal MRP provider wrapper (FastAPI).
19
+
20
+ This creates a small project you can deploy anywhere. It implements:
21
+ - GET /.well-known/mrp.json
22
+ - GET /mrp/manifest
23
+ - POST /mrp/discover
24
+ - POST /mrp/execute
25
+
26
+ Then you can self-register via: mrpd publish --manifest-url https://YOUR_DOMAIN/mrp/manifest
27
+ """
28
+
29
+ out = Path(out_dir).resolve()
30
+ out.mkdir(parents=True, exist_ok=True)
31
+
32
+ # Basic manifest (single capability)
33
+ manifest = {
34
+ "capability_id": f"capability:mrp/{capability}",
35
+ "capability": capability,
36
+ "version": "0.1",
37
+ "tags": ["mrp"],
38
+ "inputs": [{"type": "json"}],
39
+ "outputs": [{"type": "json"}],
40
+ "constraints": {"policy": policy or []},
41
+ "proofs_required": [],
42
+ "endpoints": {
43
+ "discover": "/mrp/discover",
44
+ "negotiate": "/mrp/negotiate",
45
+ "execute": "/mrp/execute",
46
+ },
47
+ }
48
+
49
+ (out / "mrp").mkdir(exist_ok=True)
50
+ (out / "mrp" / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
51
+
52
+ # A tiny FastAPI wrapper.
53
+ app_py = f'''from __future__ import annotations
54
+
55
+ import json
56
+ from pathlib import Path
57
+
58
+ from fastapi import FastAPI
59
+
60
+ # Minimal MRP provider scaffold
61
+ # - Serve /.well-known/mrp.json + /mrp/manifest
62
+ # - Implement /mrp/discover + /mrp/execute
63
+
64
+ APP_DIR = Path(__file__).resolve().parent
65
+ MANIFEST_PATH = APP_DIR / "mrp" / "manifest.json"
66
+
67
+ app = FastAPI(title={name!r})
68
+
69
+
70
+ def _manifest() -> dict:
71
+ return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
72
+
73
+
74
+ @app.get("/.well-known/mrp.json")
75
+ def well_known() -> dict:
76
+ # For deployed services, set manifest_url to an absolute URL.
77
+ return {{
78
+ "mrp_version": "0.1",
79
+ "id": {provider_id!r},
80
+ "name": {name!r},
81
+ "description": {description!r},
82
+ "manifest_url": "/mrp/manifest",
83
+ "capabilities": [{capability!r}],
84
+ }}
85
+
86
+
87
+ @app.get("/mrp/manifest")
88
+ def mrp_manifest() -> dict:
89
+ return _manifest()
90
+
91
+
92
+ @app.post("/mrp/discover")
93
+ def mrp_discover(envelope: dict) -> dict:
94
+ # Minimal discover: always offer the single capability
95
+ req_id = envelope.get("msg_id")
96
+ sender = (envelope.get("sender") or {{}}).get("id")
97
+
98
+ offer = {{
99
+ "route_id": f"route:{provider_id}/{capability}@0.1",
100
+ "capability": {capability!r},
101
+ "constraints": _manifest().get("constraints", {{}}),
102
+ "cost": {{"unit": "usd", "estimate": 0}},
103
+ "latency_ms": 0,
104
+ "proofs": [],
105
+ }}
106
+
107
+ return {{
108
+ "mrp_version": "0.1",
109
+ "msg_id": str(__import__("uuid").uuid4()),
110
+ "msg_type": "OFFER",
111
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
112
+ "sender": {{"id": {provider_id!r}}},
113
+ "receiver": {{"id": sender}} if sender else None,
114
+ "in_reply_to": req_id,
115
+ "payload": {{"offers": [offer]}},
116
+ }}
117
+
118
+
119
+ @app.post("/mrp/execute")
120
+ def mrp_execute(envelope: dict) -> dict:
121
+ # TODO: implement your actual business logic here.
122
+ req_id = envelope.get("msg_id")
123
+ sender = (envelope.get("sender") or {{}}).get("id")
124
+
125
+ payload = envelope.get("payload") or {{}}
126
+ inputs = payload.get("inputs") or []
127
+
128
+ # Convention: one json input with arbitrary value
129
+ inp = next((i for i in inputs if isinstance(i, dict) and i.get("type") == "json"), None)
130
+ value = inp.get("value") if isinstance(inp, dict) else None
131
+
132
+ output = {{"ok": True, "echo": value}}
133
+
134
+ return {{
135
+ "mrp_version": "0.1",
136
+ "msg_id": str(__import__("uuid").uuid4()),
137
+ "msg_type": "EVIDENCE",
138
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
139
+ "sender": {{"id": {provider_id!r}}},
140
+ "receiver": {{"id": sender}} if sender else None,
141
+ "in_reply_to": req_id,
142
+ "payload": {{
143
+ "route_id": payload.get("route_id"),
144
+ "outputs": [{{"type": "json", "value": output}}],
145
+ "provenance": {{"citations": [], "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z"}},
146
+ "usage": {{"tokens_in_est": 0, "tokens_out_est": 0}},
147
+ "job_id": (payload.get("job") or {{}}).get("id"),
148
+ }},
149
+ }}
150
+ '''
151
+
152
+ (out / "app.py").write_text(app_py, encoding="utf-8")
153
+
154
+ (out / "README.md").write_text(
155
+ (
156
+ f"# {name}\n\n"
157
+ "This folder was generated by `mrpd init-provider`.\n\n"
158
+ "## Run locally\n\n"
159
+ "```bash\n"
160
+ "python -m venv .venv\n"
161
+ "# Windows: .\\.venv\\Scripts\\Activate.ps1\n"
162
+ "pip install fastapi uvicorn httpx\n"
163
+ "uvicorn app:app --host 127.0.0.1 --port 8787 --reload\n"
164
+ "```\n\n"
165
+ "## Self-register (HTTP-01)\n\n"
166
+ "After deploying to a public HTTPS domain, run:\n\n"
167
+ "```bash\n"
168
+ "mrpd publish --manifest-url https://YOUR_DOMAIN/mrp/manifest\n"
169
+ "```\n"
170
+ ),
171
+ encoding="utf-8",
172
+ )
173
+
174
+ typer.echo(f"Scaffolded provider in: {out}")
175
+ typer.echo("Next: edit app.py to call your real app logic.")
176
+ typer.echo("Then deploy, and run: mrpd publish --manifest-url https://YOUR_DOMAIN/mrp/manifest")
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from mrpd.commands.bridge_mcp import bridge_mcp
8
+ from mrpd.commands.publish import publish
9
+
10
+
11
+ def mrpify_mcp(
12
+ tools_json: str,
13
+ out_dir: str,
14
+ provider_id: str,
15
+ mcp_command: str,
16
+ mcp_args: list[str],
17
+ public_base_url: str | None,
18
+ do_publish: bool,
19
+ yes: bool,
20
+ registry: str | None,
21
+ poll_seconds: float,
22
+ ) -> None:
23
+ """End-to-end developer flow for MCP -> MRP.
24
+
25
+ 1) Generate a bridge wrapper (FastAPI) from an MCP tools list
26
+ 2) Print the manifest URLs you should publish after deploying
27
+ 3) Optionally run `mrpd publish` for each manifest URL (requires public HTTPS deploy)
28
+
29
+ Notes:
30
+ - v0 uses one MRP capability per MCP tool name
31
+ - publish requires the manifest URL to be publicly reachable on HTTPS
32
+ """
33
+
34
+ bridge_mcp(
35
+ tools_json=tools_json,
36
+ out_dir=out_dir,
37
+ provider_id=provider_id,
38
+ mcp_command=mcp_command,
39
+ mcp_args=mcp_args,
40
+ )
41
+
42
+ manifests_dir = Path(out_dir).resolve() / "mrp_manifests"
43
+ caps = sorted([p.stem for p in manifests_dir.glob("*.json")])
44
+ if not caps:
45
+ raise typer.Exit(code=2)
46
+
47
+ typer.echo("")
48
+ typer.echo("Generated capabilities:")
49
+ for c in caps:
50
+ typer.echo(f"- {c}")
51
+
52
+ typer.echo("")
53
+ typer.echo("Next: deploy the generated app, then publish these manifest URLs:")
54
+
55
+ if public_base_url:
56
+ base = public_base_url.rstrip("/")
57
+ urls = [f"{base}/mrp/manifest/{c}" for c in caps]
58
+ for u in urls:
59
+ typer.echo(f" mrpd publish --manifest-url {u}")
60
+ else:
61
+ urls = []
62
+ typer.echo(" (pass --public-base-url https://YOUR_DOMAIN to print full publish commands)")
63
+
64
+ if do_publish:
65
+ if not public_base_url:
66
+ typer.echo("\nERROR: --publish requires --public-base-url")
67
+ raise typer.Exit(code=2)
68
+
69
+ if not yes:
70
+ ok = typer.confirm(
71
+ f"Run mrpd publish now for {len(urls)} manifest URLs? (requires public HTTPS + http-01 hosting)",
72
+ default=False,
73
+ )
74
+ if not ok:
75
+ raise typer.Exit(code=1)
76
+
77
+ for u in urls:
78
+ typer.echo("")
79
+ typer.echo(f"Publishing: {u}")
80
+ publish(manifest_url=u, registry=registry, poll_seconds=poll_seconds)
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from mrpd.commands.bridge_openapi import bridge_openapi
8
+ from mrpd.commands.publish import publish
9
+
10
+
11
+ def mrpify_openapi(
12
+ spec: str,
13
+ out_dir: str,
14
+ provider_id: str,
15
+ backend_base_url: str | None,
16
+ capability_prefix: str | None,
17
+ public_base_url: str | None,
18
+ do_publish: bool,
19
+ yes: bool,
20
+ registry: str | None,
21
+ poll_seconds: float,
22
+ ) -> None:
23
+ """End-to-end developer flow for OpenAPI -> MRP.
24
+
25
+ 1) Generate a bridge wrapper (FastAPI) from an OpenAPI spec
26
+ 2) Print the manifest URLs you should publish after deploying
27
+ 3) Optionally run `mrpd publish` for each manifest URL (requires public HTTPS deploy)
28
+
29
+ Notes:
30
+ - v0 uses one MRP capability per OpenAPI operationId (optionally prefixed)
31
+ - publish requires the manifest URL to be publicly reachable on HTTPS
32
+ """
33
+
34
+ bridge_openapi(
35
+ spec=spec,
36
+ out_dir=out_dir,
37
+ provider_id=provider_id,
38
+ backend_base_url=backend_base_url,
39
+ capability_prefix=capability_prefix,
40
+ )
41
+
42
+ manifests_dir = Path(out_dir).resolve() / "mrp_manifests"
43
+ caps = sorted([p.stem for p in manifests_dir.glob("*.json")])
44
+ if not caps:
45
+ raise typer.Exit(code=2)
46
+
47
+ typer.echo("")
48
+ typer.echo("Generated capabilities:")
49
+ for c in caps:
50
+ typer.echo(f"- {c}")
51
+
52
+ typer.echo("")
53
+ typer.echo("Next: deploy the generated app, then publish these manifest URLs:")
54
+
55
+ if public_base_url:
56
+ base = public_base_url.rstrip("/")
57
+ urls = [f"{base}/mrp/manifest/{c}" for c in caps]
58
+ for u in urls:
59
+ typer.echo(f" mrpd publish --manifest-url {u}")
60
+ else:
61
+ urls = []
62
+ typer.echo(" (pass --public-base-url https://YOUR_DOMAIN to print full publish commands)")
63
+
64
+ if do_publish:
65
+ if not public_base_url:
66
+ typer.echo("\nERROR: --publish requires --public-base-url")
67
+ raise typer.Exit(code=2)
68
+
69
+ if not yes:
70
+ ok = typer.confirm(
71
+ f"Run mrpd publish now for {len(urls)} manifest URLs? (requires public HTTPS + http-01 hosting)",
72
+ default=False,
73
+ )
74
+ if not ok:
75
+ raise typer.Exit(code=1)
76
+
77
+ for u in urls:
78
+ typer.echo("")
79
+ typer.echo(f"Publishing: {u}")
80
+ publish(manifest_url=u, registry=registry, poll_seconds=poll_seconds)