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.
- mrpd/__init__.py +1 -0
- mrpd/adapters/__init__.py +1 -0
- mrpd/api/app.py +8 -0
- mrpd/api/routes.py +200 -0
- mrpd/cli.py +182 -0
- mrpd/commands/__init__.py +1 -0
- mrpd/commands/bridge_mcp.py +189 -0
- mrpd/commands/bridge_openapi.py +279 -0
- mrpd/commands/init_provider.py +176 -0
- mrpd/commands/mrpify_mcp.py +80 -0
- mrpd/commands/mrpify_openapi.py +80 -0
- mrpd/commands/publish.py +102 -0
- mrpd/commands/route.py +126 -0
- mrpd/commands/run.py +169 -0
- mrpd/commands/serve.py +13 -0
- mrpd/commands/validate.py +71 -0
- mrpd/core/__init__.py +1 -0
- mrpd/core/artifacts.py +39 -0
- mrpd/core/config.py +26 -0
- mrpd/core/defaults.py +7 -0
- mrpd/core/envelopes.py +29 -0
- mrpd/core/errors.py +37 -0
- mrpd/core/evidence.py +17 -0
- mrpd/core/models.py +37 -0
- mrpd/core/provider.py +100 -0
- mrpd/core/registry.py +115 -0
- mrpd/core/schema.py +64 -0
- mrpd/core/scoring.py +92 -0
- mrpd/core/util.py +27 -0
- mrpd/spec/__init__.py +1 -0
- mrpd/spec/fixtures/README.md +8 -0
- mrpd/spec/fixtures/invalid/bad_msg_type.json +8 -0
- mrpd/spec/fixtures/invalid/missing_required_fields.json +7 -0
- mrpd/spec/fixtures/valid/discover.json +13 -0
- mrpd/spec/fixtures/valid/error.json +13 -0
- mrpd/spec/fixtures/valid/offer.json +20 -0
- mrpd/spec/schemas/envelope.schema.json +102 -0
- mrpd/spec/schemas/manifest.schema.json +52 -0
- mrpd/spec/schemas/payloads/discover.schema.json +23 -0
- mrpd/spec/schemas/payloads/error.schema.json +15 -0
- mrpd/spec/schemas/payloads/evidence.schema.json +17 -0
- mrpd/spec/schemas/payloads/execute.schema.json +14 -0
- mrpd/spec/schemas/payloads/negotiate.schema.json +15 -0
- mrpd/spec/schemas/payloads/offer.schema.json +30 -0
- mrpd/spec/schemas/types/artifact.schema.json +15 -0
- mrpd/spec/schemas/types/input.schema.json +20 -0
- mrpd-0.1.0.dist-info/METADATA +104 -0
- mrpd-0.1.0.dist-info/RECORD +51 -0
- mrpd-0.1.0.dist-info/WHEEL +5 -0
- mrpd-0.1.0.dist-info/entry_points.txt +2 -0
- 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
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}")
|