File size: 7,128 Bytes
6cefeec
 
e57d3fe
 
6cefeec
 
 
 
 
 
 
 
2e3ca88
6cefeec
e57d3fe
6cefeec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e57d3fe
6cefeec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1c195f
30bf877
 
6cefeec
32e3729
e1c195f
6cefeec
 
30bf877
e57d3fe
 
6cefeec
2e3ca88
 
 
 
 
 
 
 
 
e57d3fe
30bf877
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e57d3fe
 
 
 
 
 
6cefeec
 
 
 
 
e57d3fe
6cefeec
30bf877
32e3729
30bf877
 
 
2e3ca88
32e3729
 
 
 
 
 
 
 
 
30bf877
 
 
 
 
32e3729
30bf877
6cefeec
 
c631558
 
 
 
 
6cefeec
e1c195f
6cefeec
 
e1c195f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6cefeec
 
e57d3fe
6cefeec
 
 
 
 
 
 
e57d3fe
6cefeec
 
 
 
 
 
e57d3fe
 
 
6cefeec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e57d3fe
6cefeec
 
 
 
 
 
 
 
 
 
e57d3fe
6cefeec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18eb9c2
6cefeec
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
from __future__ import annotations

# ruff: noqa: E402

import json
import os
import sys
import traceback
from pathlib import Path

from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import PlainTextResponse


def _discover_workspace_root() -> Path:
    env_root = os.getenv("CODE_TOOLS_ROOT")
    if env_root:
        return Path(env_root).expanduser().resolve()

    script_root = Path(__file__).resolve().parents[1]
    if (script_root / ".prefab").exists():
        return script_root

    return (Path.home() / "source/code_tools").resolve()


WORKSPACE_ROOT = _discover_workspace_root()
PREFAB_ROOT = WORKSPACE_ROOT / ".prefab"
PREFAB_SRC = Path(os.getenv("PREFAB_SRC", str(Path.home() / "source/prefab/src")))
SCRIPTS_DIR = Path(__file__).resolve().parent
CARDS_DIR = PREFAB_ROOT / "agent-cards"
CONFIG_PATH = PREFAB_ROOT / "fastagent.config.yaml"
RAW_CARD_FILE = CARDS_DIR / "hub_search_raw.md"
EXPANDED_CARDS_DIR = CARDS_DIR
RAW_AGENT = "hub_search_raw"

HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "9999"))
PATH = os.getenv("MCP_PATH", "/mcp")
CORS_MIDDLEWARE = [
    Middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["*"],
        allow_headers=["*"],
        expose_headers=["mcp-session-id"],
    )
]

os.chdir(WORKSPACE_ROOT)
if PREFAB_SRC.exists() and str(PREFAB_SRC) not in sys.path:
    sys.path.insert(0, str(PREFAB_SRC))
if str(SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPTS_DIR))

from fast_agent import FastAgent
from fast_agent.mcp.auth.context import request_bearer_token
from fast_agent.mcp.auth.middleware import HFAuthHeaderMiddleware
from fast_agent.mcp.auth.presence import PresenceTokenVerifier
from fastmcp import FastMCP
from fastmcp.server.auth.auth import RemoteAuthProvider
from fastmcp.server.dependencies import get_access_token
from fastmcp.tools import ToolResult
from mcp.types import TextContent
from pydantic import AnyHttpUrl
from card_includes import materialize_expanded_card
from prefab_hub_ui import build_runtime_wire, error_wire, parse_runtime_payload


class _RootResourceRemoteAuthProvider(RemoteAuthProvider):
    """Advertise the Space root as the protected resource."""

    def _get_resource_url(self, path: str | None = None) -> AnyHttpUrl | None:
        del path
        return self.base_url



def _get_oauth_config() -> tuple[str | None, list[str], str]:
    oauth_provider = os.environ.get("FAST_AGENT_SERVE_OAUTH", "").lower()
    if oauth_provider in ("hf", "huggingface"):
        oauth_provider = "huggingface"
    elif not oauth_provider:
        oauth_provider = None

    oauth_scopes_str = os.environ.get("FAST_AGENT_OAUTH_SCOPES", "")
    oauth_scopes = [s.strip() for s in oauth_scopes_str.split(",") if s.strip()] or ["access"]
    resource_url = os.environ.get(
        "FAST_AGENT_OAUTH_RESOURCE_URL",
        f"http://localhost:{PORT}",
    )
    return oauth_provider, oauth_scopes, resource_url


EXPANDED_RAW_CARD_FILE = materialize_expanded_card(
    RAW_CARD_FILE,
    workspace_root=WORKSPACE_ROOT,
    out_dir=EXPANDED_CARDS_DIR,
)

fast = FastAgent(
    "hub-search-prefab",
    config_path=str(CONFIG_PATH),
    parse_cli_args=False,
)
fast.load_agents(EXPANDED_RAW_CARD_FILE)

_oauth_provider, _oauth_scopes, _oauth_resource_url = _get_oauth_config()
_auth_provider = None
_middleware = list(CORS_MIDDLEWARE)

if _oauth_provider == "huggingface":
    _auth_provider = _RootResourceRemoteAuthProvider(
        token_verifier=PresenceTokenVerifier(
            provider="huggingface",
            scopes=_oauth_scopes,
        ),
        authorization_servers=[AnyHttpUrl("https://huggingface.co")],
        base_url=AnyHttpUrl(_oauth_resource_url),
        scopes_supported=_oauth_scopes,
        resource_name="gen-ui",
        resource_documentation=AnyHttpUrl("https://huggingface.co/spaces/evalstate/gen-ui"),
    )
    _middleware.append(Middleware(HFAuthHeaderMiddleware))

mcp = FastMCP(
    "hub-search-prefab",
    auth=_auth_provider,
)


@mcp.custom_route("/", methods=["GET"])
async def root_info(request) -> PlainTextResponse:
    return PlainTextResponse("gen-ui MCP server. Use /mcp for MCP and /.well-known/oauth-protected-resource for auth discovery.")


async def _run_raw(query: str) -> str:
    return await _run_agent(RAW_AGENT, query)



def _get_request_bearer_token() -> str | None:
    access_token = get_access_token()
    if access_token is None:
        return None
    return access_token.token


async def _run_agent(agent_name: str, query: str) -> str:
    saved_token = request_bearer_token.set(_get_request_bearer_token())
    try:
        async with fast.run() as agents:
            return await getattr(agents, agent_name).send(query)
    finally:
        request_bearer_token.reset(saved_token)



def _wire_tool_result(wire: dict[str, object]) -> ToolResult:
    return ToolResult(
        content=[TextContent(type="text", text="[Rendered Prefab UI]")],
        structured_content=wire,
    )



def _render_query_wire(query: str, raw_text: str) -> dict[str, object]:
    payload = parse_runtime_payload(raw_text)
    return build_runtime_wire(query, payload)


async def _build_query_wire(query: str) -> dict[str, object]:
    raw = await _run_raw(query)
    return _render_query_wire(query, raw)



def _missing_query_json() -> str:
    return json.dumps(
        {
            "result": None,
            "meta": {
                "ok": False,
                "error": "Missing required argument: query",
            },
        }
    )


@mcp.tool(app=True)
async def hub_search_prefab(query: str) -> ToolResult:
    """Run the Prefab UI service with deterministic rendering over raw Hub output."""
    try:
        wire = await _build_query_wire(query)
    except Exception as exc:  # noqa: BLE001
        traceback.print_exc()
        wire = error_wire(str(exc))
    return _wire_tool_result(wire)


@mcp.tool
async def hub_search_prefab_wire(query: str | None = None) -> str:
    """Return final deterministic Prefab wire JSON for a Hub query."""
    if not query:
        return json.dumps(error_wire("Missing required argument: query"), ensure_ascii=False)
    try:
        wire = await _build_query_wire(query)
        return json.dumps(wire, ensure_ascii=False)
    except Exception as exc:  # noqa: BLE001
        traceback.print_exc()
        return json.dumps(error_wire(str(exc)), ensure_ascii=False)


@mcp.tool
async def hub_search_raw_debug(query: str | None = None) -> str:
    """Return the raw live-service payload from the raw Hub search agent."""
    if not query:
        return _missing_query_json()
    try:
        return await _run_raw(query)
    except Exception as exc:  # noqa: BLE001
        traceback.print_exc()
        return json.dumps({"result": None, "meta": {"ok": False, "error": str(exc)}})



def main() -> None:
    mcp.run(
        "streamable-http",
        host=HOST,
        port=PORT,
        path=PATH,
        stateless_http=True,
        middleware=_middleware,
    )


if __name__ == "__main__":
    main()