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
mrpd/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1 @@
1
+ __all__ = []
mrpd/api/app.py ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from mrpd.api.routes import router
6
+
7
+ app = FastAPI(title="mrpd (Moltrouter Protocol Daemon)")
8
+ app.include_router(router)
mrpd/api/routes.py ADDED
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from mrpd.core.errors import mrp_error
6
+ from mrpd.core.schema import validate_envelope
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ def response_envelope(envelope: dict, *, msg_type: str, payload: dict) -> dict:
12
+ """Build a response envelope.
13
+
14
+ IMPORTANT: response message ids must be new, and in_reply_to must reference
15
+ the triggering request msg_id.
16
+ """
17
+
18
+ from mrpd.core.util import utc_now_rfc3339
19
+ import uuid
20
+
21
+ sender_id = (envelope.get("sender") or {}).get("id")
22
+ req_msg_id = envelope.get("msg_id")
23
+
24
+ resp = {
25
+ "mrp_version": envelope.get("mrp_version", "0.1"),
26
+ "msg_id": str(uuid.uuid4()),
27
+ "msg_type": msg_type,
28
+ "timestamp": utc_now_rfc3339(),
29
+ "sender": {"id": "service:mrpd"},
30
+ "payload": payload,
31
+ }
32
+ if sender_id:
33
+ resp["receiver"] = {"id": sender_id}
34
+ if req_msg_id:
35
+ resp["in_reply_to"] = req_msg_id
36
+ return resp
37
+
38
+
39
+ @router.get("/.well-known/mrp.json")
40
+ async def well_known() -> dict:
41
+ return {
42
+ "mrp_version": "0.1",
43
+ "capabilities": ["summarize_url"],
44
+ "manifest_url": "/mrp/manifest",
45
+ }
46
+
47
+
48
+ @router.get("/mrp/manifest")
49
+ async def manifest() -> dict:
50
+ # Provider manifest for the built-in demo capability.
51
+ # NOTE: this endpoint returns relative URLs; clients may prefer absolute.
52
+ # Our `mrpd run` prefers manifests from registry entries (absolute endpoints).
53
+ return {
54
+ "capability_id": "capability:mrp/summarize_url",
55
+ "capability": "summarize_url",
56
+ "version": "0.1",
57
+ "tags": ["mrp", "summarize", "web"],
58
+ "inputs": [{"type": "url"}],
59
+ "outputs": [{"type": "markdown"}, {"type": "artifact"}],
60
+ "constraints": {"policy": ["no_pii"]},
61
+ "proofs_required": [],
62
+ "endpoints": {
63
+ "discover": "/mrp/discover",
64
+ "negotiate": "/mrp/negotiate",
65
+ "execute": "/mrp/execute",
66
+ },
67
+ }
68
+
69
+
70
+ @router.post("/mrp/hello")
71
+ async def hello(envelope: dict) -> dict:
72
+ try:
73
+ validate_envelope(envelope)
74
+ except Exception as e:
75
+ sender_id = (envelope.get("sender") or {}).get("id")
76
+ msg_id = envelope.get("msg_id")
77
+ return mrp_error(
78
+ msg_id=msg_id,
79
+ timestamp=envelope.get("timestamp"),
80
+ receiver_id=sender_id,
81
+ in_reply_to=msg_id,
82
+ code="MRP_INVALID_REQUEST",
83
+ message=str(e),
84
+ retryable=False,
85
+ )
86
+
87
+ return response_envelope(
88
+ envelope,
89
+ msg_type="HELLO",
90
+ payload={"ok": True, "schemas": ["0.1"]},
91
+ )
92
+
93
+
94
+ @router.post("/mrp/discover")
95
+ async def discover(envelope: dict) -> dict:
96
+ try:
97
+ validate_envelope(envelope)
98
+ except Exception as e:
99
+ sender_id = (envelope.get("sender") or {}).get("id")
100
+ msg_id = envelope.get("msg_id")
101
+ return mrp_error(
102
+ msg_id=msg_id,
103
+ timestamp=envelope.get("timestamp"),
104
+ receiver_id=sender_id,
105
+ in_reply_to=msg_id,
106
+ code="MRP_INVALID_REQUEST",
107
+ message=str(e),
108
+ retryable=False,
109
+ )
110
+
111
+ from mrpd.core.provider import offers_for_discover
112
+
113
+ offers = offers_for_discover(envelope.get("payload") or {})
114
+ return response_envelope(
115
+ envelope,
116
+ msg_type="OFFER",
117
+ payload={"offers": offers},
118
+ )
119
+
120
+
121
+ @router.post("/mrp/negotiate")
122
+ async def negotiate(envelope: dict) -> dict:
123
+ try:
124
+ validate_envelope(envelope)
125
+ except Exception as e:
126
+ sender_id = (envelope.get("sender") or {}).get("id")
127
+ msg_id = envelope.get("msg_id")
128
+ return mrp_error(
129
+ msg_id=msg_id,
130
+ timestamp=envelope.get("timestamp"),
131
+ receiver_id=sender_id,
132
+ in_reply_to=msg_id,
133
+ code="MRP_INVALID_REQUEST",
134
+ message=str(e),
135
+ retryable=False,
136
+ )
137
+
138
+ return response_envelope(
139
+ envelope,
140
+ msg_type="NEGOTIATE",
141
+ payload={"accepted": False, "reason": "not implemented"},
142
+ )
143
+
144
+
145
+ @router.post("/mrp/execute")
146
+ async def execute(envelope: dict) -> dict:
147
+ try:
148
+ validate_envelope(envelope)
149
+ except Exception as e:
150
+ sender_id = (envelope.get("sender") or {}).get("id")
151
+ msg_id = envelope.get("msg_id")
152
+ return mrp_error(
153
+ msg_id=msg_id,
154
+ timestamp=envelope.get("timestamp"),
155
+ receiver_id=sender_id,
156
+ in_reply_to=msg_id,
157
+ code="MRP_INVALID_REQUEST",
158
+ message=str(e),
159
+ retryable=False,
160
+ )
161
+
162
+ from mrpd.core.provider import execute_summarize_url
163
+
164
+ payload = envelope.get("payload") or {}
165
+ route_id = payload.get("route_id")
166
+ inputs = payload.get("inputs") or []
167
+ job_id = (payload.get("job") or {}).get("id")
168
+
169
+ try:
170
+ if route_id == "route:mrpd/summarize_url@0.1":
171
+ evidence = await execute_summarize_url(inputs)
172
+ else:
173
+ sender_id = (envelope.get("sender") or {}).get("id")
174
+ msg_id = envelope.get("msg_id")
175
+ return mrp_error(
176
+ msg_id=msg_id,
177
+ timestamp=envelope.get("timestamp"),
178
+ receiver_id=sender_id,
179
+ in_reply_to=msg_id,
180
+ code="MRP_INVALID_REQUEST",
181
+ message=f"Unknown route_id: {route_id}",
182
+ retryable=False,
183
+ )
184
+ except Exception as e:
185
+ sender_id = (envelope.get("sender") or {}).get("id")
186
+ msg_id = envelope.get("msg_id")
187
+ return mrp_error(
188
+ msg_id=msg_id,
189
+ timestamp=envelope.get("timestamp"),
190
+ receiver_id=sender_id,
191
+ in_reply_to=msg_id,
192
+ code="MRP_INTERNAL_ERROR",
193
+ message=str(e),
194
+ retryable=False,
195
+ )
196
+
197
+ response_payload = {"route_id": route_id, **evidence}
198
+ if job_id:
199
+ response_payload["job_id"] = job_id
200
+ return response_envelope(envelope, msg_type="EVIDENCE", payload=response_payload)
mrpd/cli.py ADDED
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from mrpd.commands.bridge_mcp import bridge_mcp
6
+ from mrpd.commands.bridge_openapi import bridge_openapi
7
+ from mrpd.commands.init_provider import init_provider
8
+ from mrpd.commands.mrpify_mcp import mrpify_mcp
9
+ from mrpd.commands.mrpify_openapi import mrpify_openapi
10
+ from mrpd.commands.publish import publish
11
+ from mrpd.commands.route import route
12
+ from mrpd.commands.run import run
13
+ from mrpd.commands.serve import serve
14
+ from mrpd.commands.validate import validate
15
+
16
+ app = typer.Typer(add_completion=False)
17
+
18
+ bridge_app = typer.Typer(help="Generate MRP provider wrappers (bridges)")
19
+ app.add_typer(bridge_app, name="bridge")
20
+
21
+ mrpify_app = typer.Typer(help="MRP-ify existing systems with a guided flow")
22
+ app.add_typer(mrpify_app, name="mrpify")
23
+
24
+
25
+ @app.command()
26
+ def version() -> None:
27
+ """Print version."""
28
+ typer.echo("mrpd 0.1.0")
29
+
30
+
31
+ @app.command(name="serve")
32
+ def serve_cmd(
33
+ host: str = typer.Option("127.0.0.1", "--host"),
34
+ port: int = typer.Option(8787, "--port"),
35
+ reload: bool = typer.Option(False, "--reload"),
36
+ ) -> None:
37
+ """Run the MRP HTTP server."""
38
+ serve(host=host, port=port, reload=reload)
39
+
40
+
41
+ @app.command(name="validate")
42
+ def validate_cmd(
43
+ path: str = typer.Option("-", "--path", help="JSON file path or '-' for stdin"),
44
+ fixtures: bool = typer.Option(False, "--fixtures", help="Validate bundled fixtures (valid must pass, invalid must fail)"),
45
+ ) -> None:
46
+ """Validate an MRP envelope against the bundled JSON Schemas."""
47
+ validate(path, fixtures=fixtures)
48
+
49
+
50
+ @app.command(name="route")
51
+ def route_cmd(
52
+ intent: str = typer.Argument(..., help="High-level intent (human text)"),
53
+ capability: str | None = typer.Option(None, "--capability", help="Capability filter"),
54
+ policy: str | None = typer.Option(None, "--policy", help="Policy filter"),
55
+ registry: str | None = typer.Option(None, "--registry", help="Registry base URL (default: https://www.moltrouter.dev)"),
56
+ bootstrap_raw: str | None = typer.Option(None, "--bootstrap-raw", help="Override fallback raw registry JSON (URL or file://path)"),
57
+ limit: int = typer.Option(10, "--limit", min=1, max=50),
58
+ ) -> None:
59
+ """Query registry + rank candidates for an intent."""
60
+ route(intent=intent, capability=capability, policy=policy, registry=registry, limit=limit, bootstrap_raw=bootstrap_raw)
61
+
62
+
63
+ @app.command(name="run")
64
+ def run_cmd(
65
+ intent: str = typer.Argument(..., help="High-level intent (human text)"),
66
+ url: str = typer.Option(..., "--url", help="URL input"),
67
+ capability: str = typer.Option("summarize_url", "--capability", help="Capability to request"),
68
+ policy: str | None = typer.Option(None, "--policy", help="Policy requirement"),
69
+ registry: str | None = typer.Option(None, "--registry", help="Registry base URL (default: https://www.moltrouter.dev)"),
70
+ manifest_url: str | None = typer.Option(None, "--manifest-url", help="Skip registry and use this provider manifest URL (useful for local testing)"),
71
+ max_tokens: int | None = typer.Option(None, "--max-tokens", help="Soft max context tokens (constraint hint)"),
72
+ max_cost: float | None = typer.Option(None, "--max-cost", help="Max cost (constraint hint)"),
73
+ ) -> None:
74
+ """End-to-end: DISCOVER -> EXECUTE against the best matching provider."""
75
+ run(intent=intent, url=url, capability=capability, policy=policy, registry=registry, manifest_url=manifest_url, max_tokens=max_tokens, max_cost=max_cost)
76
+
77
+
78
+ @app.command(name="publish")
79
+ def publish_cmd(
80
+ manifest_url: str = typer.Option(..., "--manifest-url", help="Provider manifest URL to self-register"),
81
+ registry: str | None = typer.Option(None, "--registry", help="Registry base URL (default: https://www.moltrouter.dev)"),
82
+ poll_seconds: float = typer.Option(5.0, "--poll-seconds", min=1.0, max=60.0),
83
+ ) -> None:
84
+ """Self-register a provider in the public registry using HTTP-01 challenge."""
85
+ publish(manifest_url=manifest_url, registry=registry, poll_seconds=poll_seconds)
86
+
87
+
88
+ @app.command(name="init-provider")
89
+ def init_provider_cmd(
90
+ out_dir: str = typer.Option("./mrp-provider", "--out-dir", help="Output directory"),
91
+ capability: str = typer.Option(..., "--capability", help="Capability name (e.g. summarize_url)"),
92
+ provider_id: str = typer.Option("service:example/provider", "--provider-id", help="Provider sender id"),
93
+ name: str = typer.Option("MRP Provider", "--name"),
94
+ description: str = typer.Option("", "--description"),
95
+ policy: list[str] = typer.Option([], "--policy", help="Policy strings (repeatable)")
96
+ ) -> None:
97
+ """Scaffold a minimal FastAPI MRP provider wrapper."""
98
+ init_provider(out_dir=out_dir, capability=capability, provider_id=provider_id, name=name, description=description, policy=policy)
99
+
100
+
101
+ @bridge_app.command(name="openapi")
102
+ def bridge_openapi_cmd(
103
+ spec: str = typer.Option(..., "--spec", help="Path to OpenAPI spec (json/yaml)"),
104
+ out_dir: str = typer.Option("./mrp-openapi-bridge", "--out-dir", help="Output directory"),
105
+ provider_id: str = typer.Option("service:openapi/bridge", "--provider-id", help="Provider sender id"),
106
+ backend_base_url: str | None = typer.Option(None, "--backend-base-url", help="Override OpenAPI servers[0].url"),
107
+ capability_prefix: str | None = typer.Option(None, "--capability-prefix", help="Prefix for generated capabilities (e.g. svc_)"),
108
+ ) -> None:
109
+ """Generate an MRP provider wrapper from an OpenAPI spec."""
110
+ bridge_openapi(spec=spec, out_dir=out_dir, provider_id=provider_id, backend_base_url=backend_base_url, capability_prefix=capability_prefix)
111
+
112
+
113
+ @bridge_app.command(name="mcp")
114
+ def bridge_mcp_cmd(
115
+ tools_json: str = typer.Option(..., "--tools-json", help="Path to MCP tools list JSON"),
116
+ out_dir: str = typer.Option("./mrp-mcp-bridge", "--out-dir", help="Output directory"),
117
+ provider_id: str = typer.Option("service:mcp/bridge", "--provider-id", help="Provider sender id"),
118
+ mcp_command: str = typer.Option(..., "--mcp-command", help="MCP server command (stdio)"),
119
+ mcp_args: list[str] = typer.Option([], "--mcp-arg", help="MCP server argument (repeatable)"),
120
+ ) -> None:
121
+ """Generate an MRP provider wrapper scaffold from an MCP tool list."""
122
+ bridge_mcp(tools_json=tools_json, out_dir=out_dir, provider_id=provider_id, mcp_command=mcp_command, mcp_args=mcp_args)
123
+
124
+
125
+ @mrpify_app.command(name="openapi")
126
+ def mrpify_openapi_cmd(
127
+ spec: str = typer.Option(..., "--spec", help="Path to OpenAPI spec (json/yaml)"),
128
+ out_dir: str = typer.Option("./mrp-openapi-bridge", "--out-dir", help="Output directory"),
129
+ provider_id: str = typer.Option("service:openapi/bridge", "--provider-id", help="Provider sender id"),
130
+ backend_base_url: str | None = typer.Option(None, "--backend-base-url", help="Override OpenAPI servers[0].url"),
131
+ capability_prefix: str | None = typer.Option(None, "--capability-prefix", help="Prefix for generated capabilities (e.g. svc_)"),
132
+ public_base_url: str | None = typer.Option(None, "--public-base-url", help="Deployed public base URL, e.g. https://api.example.com"),
133
+ publish_now: bool = typer.Option(False, "--publish", help="Run mrpd publish for each generated manifest URL (requires public HTTPS)"),
134
+ yes: bool = typer.Option(False, "--yes", help="Skip confirmation prompts"),
135
+ registry: str | None = typer.Option(None, "--registry", help="Registry base URL (default: https://www.moltrouter.dev)"),
136
+ poll_seconds: float = typer.Option(5.0, "--poll-seconds", min=1.0, max=60.0),
137
+ ) -> None:
138
+ """Guided flow: OpenAPI -> MRP bridge -> (optional) publish commands."""
139
+ mrpify_openapi(
140
+ spec=spec,
141
+ out_dir=out_dir,
142
+ provider_id=provider_id,
143
+ backend_base_url=backend_base_url,
144
+ capability_prefix=capability_prefix,
145
+ public_base_url=public_base_url,
146
+ do_publish=publish_now,
147
+ yes=yes,
148
+ registry=registry,
149
+ poll_seconds=poll_seconds,
150
+ )
151
+
152
+
153
+ @mrpify_app.command(name="mcp")
154
+ def mrpify_mcp_cmd(
155
+ tools_json: str = typer.Option(..., "--tools-json", help="Path to MCP tools list JSON"),
156
+ out_dir: str = typer.Option("./mrp-mcp-bridge", "--out-dir", help="Output directory"),
157
+ provider_id: str = typer.Option("service:mcp/bridge", "--provider-id", help="Provider sender id"),
158
+ mcp_command: str = typer.Option(..., "--mcp-command", help="MCP server command (stdio)"),
159
+ mcp_args: list[str] = typer.Option([], "--mcp-arg", help="MCP server argument (repeatable)"),
160
+ public_base_url: str | None = typer.Option(None, "--public-base-url", help="Deployed public base URL, e.g. https://api.example.com"),
161
+ publish_now: bool = typer.Option(False, "--publish", help="Run mrpd publish for each generated manifest URL (requires public HTTPS)"),
162
+ yes: bool = typer.Option(False, "--yes", help="Skip confirmation prompts"),
163
+ registry: str | None = typer.Option(None, "--registry", help="Registry base URL (default: https://www.moltrouter.dev)"),
164
+ poll_seconds: float = typer.Option(5.0, "--poll-seconds", min=1.0, max=60.0),
165
+ ) -> None:
166
+ """Guided flow: MCP -> MRP bridge -> (optional) publish commands."""
167
+ mrpify_mcp(
168
+ tools_json=tools_json,
169
+ out_dir=out_dir,
170
+ provider_id=provider_id,
171
+ mcp_command=mcp_command,
172
+ mcp_args=mcp_args,
173
+ public_base_url=public_base_url,
174
+ do_publish=publish_now,
175
+ yes=yes,
176
+ registry=registry,
177
+ poll_seconds=poll_seconds,
178
+ )
179
+
180
+
181
+ if __name__ == "__main__":
182
+ app()
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+
9
+ def bridge_mcp(
10
+ tools_json: str,
11
+ out_dir: str,
12
+ provider_id: str,
13
+ mcp_command: str,
14
+ mcp_args: list[str],
15
+ ) -> None:
16
+ """Generate an MRP provider wrapper from an MCP tool list.
17
+
18
+ v0 behavior:
19
+ - one MRP capability per MCP tool name
20
+ - generates per-tool manifests served at /mrp/manifest/<tool>
21
+
22
+ NOTE: This is a scaffold only. You must implement the actual MCP execution
23
+ (stdio) inside the generated app.
24
+ """
25
+
26
+ tools = json.loads(Path(tools_json).read_text(encoding="utf-8"))
27
+ if not isinstance(tools, list) or not tools:
28
+ raise typer.Exit(code=2)
29
+
30
+ out = Path(out_dir).resolve()
31
+ out.mkdir(parents=True, exist_ok=True)
32
+ (out / "mrp_manifests").mkdir(exist_ok=True)
33
+
34
+ for t in tools:
35
+ name = t.get("name") if isinstance(t, dict) else None
36
+ if not name or not isinstance(name, str):
37
+ continue
38
+ route_id = f"route:mcp/{name}@0.1"
39
+ manifest = {
40
+ "capability_id": f"capability:mcp/{name}",
41
+ "capability": name,
42
+ "version": "0.1",
43
+ "tags": ["mrp", "mcp"],
44
+ "inputs": [{"type": "json"}],
45
+ "outputs": [{"type": "json"}],
46
+ "constraints": {"policy": []},
47
+ "proofs_required": [],
48
+ "endpoints": {"discover": "/mrp/discover", "execute": "/mrp/execute"},
49
+ "metadata": {
50
+ "mcp": {
51
+ "tool": name,
52
+ "route_id": route_id,
53
+ "stdio": {"command": mcp_command, "args": mcp_args},
54
+ }
55
+ },
56
+ }
57
+ (out / "mrp_manifests" / f"{name}.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
58
+
59
+ app_py = '''from __future__ import annotations
60
+
61
+ import json
62
+ from pathlib import Path
63
+
64
+ from fastapi import FastAPI
65
+
66
+ APP_DIR = Path(__file__).resolve().parent
67
+ MANIFEST_DIR = APP_DIR / "mrp_manifests"
68
+ MCP_COMMAND = ''' + json.dumps(mcp_command) + '''
69
+ MCP_ARGS = ''' + json.dumps(mcp_args) + '''
70
+
71
+ app = FastAPI(title="MRP MCP Bridge")
72
+
73
+
74
+ def _load_manifest(name: str) -> dict:
75
+ fp = MANIFEST_DIR / f"{name}.json"
76
+ return json.loads(fp.read_text(encoding="utf-8"))
77
+
78
+
79
+ def _tool_from_route_id(route_id: str) -> str:
80
+ # route:mcp/<tool>@0.1
81
+ if not route_id.startswith("route:mcp/"):
82
+ raise ValueError("invalid route_id")
83
+ rest = route_id[len("route:mcp/"):]
84
+ tool = rest.split("@", 1)[0]
85
+ return tool
86
+
87
+
88
+ @app.get("/.well-known/mrp.json")
89
+ def well_known() -> dict:
90
+ caps = []
91
+ for fp in sorted(MANIFEST_DIR.glob("*.json")):
92
+ try:
93
+ m = json.loads(fp.read_text(encoding="utf-8"))
94
+ c = m.get("capability")
95
+ if isinstance(c, str):
96
+ caps.append(c)
97
+ except Exception:
98
+ continue
99
+
100
+ return {
101
+ "mrp_version": "0.1",
102
+ "id": "''' + provider_id + '''",
103
+ "name": "MRP MCP Bridge",
104
+ "manifest_url": "/mrp/manifest/" + (caps[0] if caps else ""),
105
+ "capabilities": caps,
106
+ }
107
+
108
+
109
+ @app.get("/mrp/manifest/{capability}")
110
+ def mrp_manifest(capability: str) -> dict:
111
+ return _load_manifest(capability)
112
+
113
+
114
+ @app.post("/mrp/discover")
115
+ def mrp_discover(envelope: dict) -> dict:
116
+ req_id = envelope.get("msg_id")
117
+ sender = (envelope.get("sender") or {}).get("id")
118
+ constraints = (envelope.get("payload") or {}).get("constraints") or {}
119
+ wanted = constraints.get("capability")
120
+
121
+ offers = []
122
+ for fp in sorted(MANIFEST_DIR.glob("*.json")):
123
+ m = json.loads(fp.read_text(encoding="utf-8"))
124
+ cap = m.get("capability")
125
+ meta = (m.get("metadata") or {}).get("mcp") or {}
126
+ route_id = meta.get("route_id")
127
+ if wanted and cap != wanted:
128
+ continue
129
+ offers.append({
130
+ "route_id": route_id,
131
+ "capability": cap,
132
+ "constraints": m.get("constraints") or {},
133
+ "cost": {"unit": "usd", "estimate": 0},
134
+ "latency_ms": 0,
135
+ "proofs": [],
136
+ })
137
+
138
+ return {
139
+ "mrp_version": "0.1",
140
+ "msg_id": str(__import__("uuid").uuid4()),
141
+ "msg_type": "OFFER",
142
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
143
+ "sender": {"id": "''' + provider_id + '''"},
144
+ "receiver": {"id": sender} if sender else None,
145
+ "in_reply_to": req_id,
146
+ "payload": {"offers": offers[:25]},
147
+ }
148
+
149
+
150
+ @app.post("/mrp/execute")
151
+ def mrp_execute(envelope: dict) -> dict:
152
+ # TODO: implement MCP execution using MCP_COMMAND + MCP_ARGS (stdio).
153
+ req_id = envelope.get("msg_id")
154
+ sender = (envelope.get("sender") or {}).get("id")
155
+
156
+ payload = envelope.get("payload") or {}
157
+ tool = _tool_from_route_id(payload.get("route_id"))
158
+
159
+ return {
160
+ "mrp_version": "0.1",
161
+ "msg_id": str(__import__("uuid").uuid4()),
162
+ "msg_type": "ERROR",
163
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
164
+ "sender": {"id": "''' + provider_id + '''"},
165
+ "receiver": {"id": sender} if sender else None,
166
+ "in_reply_to": req_id,
167
+ "payload": {
168
+ "code": "MRP_NOT_IMPLEMENTED",
169
+ "message": f"MCP execution not implemented for tool: {tool}",
170
+ "retryable": False,
171
+ },
172
+ }
173
+ '''
174
+
175
+ (out / "app.py").write_text(app_py, encoding="utf-8")
176
+ (out / "README.md").write_text(
177
+ "# MRP MCP Bridge (scaffold)\n\n"
178
+ "Generated by `mrpd bridge mcp`.\n\n"
179
+ "This is a scaffold: you still need to implement the actual MCP connection\n"
180
+ "(stdio) in `app.py` /mrp/execute.\n\n"
181
+ "## Publish\n\n"
182
+ "After deploying, publish each manifest you want discoverable:\n\n"
183
+ "```bash\n"
184
+ "mrpd publish --manifest-url https://YOUR_DOMAIN/mrp/manifest/<tool>\n"
185
+ "```\n",
186
+ encoding="utf-8",
187
+ )
188
+
189
+ typer.echo(f"Generated MCP bridge scaffold in: {out}")