File size: 3,149 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * useMatrixHub — hook for searching and browsing the MatrixHub MCP server catalog.
 *
 * MatrixHub (https://github.com/agent-matrix/matrix-hub) is an optional
 * secondary catalog source for MCP servers, agents, and tools.
 *
 * Public endpoints used (no auth required):
 *   GET /catalog/search?q=...&type=mcp_server&limit=...
 *   GET /catalog?type=mcp_server&limit=...&offset=...
 *
 * Phase 11 — fully additive, optional feature.
 */
import { useCallback, useEffect, useMemo, useState } from 'react';
export function useMatrixHub({ hubUrl, enabled = true }) {
    const [items, setItems] = useState([]);
    const [total, setTotal] = useState(0);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    const [search, setSearch] = useState('');
    const [healthy, setHealthy] = useState(null);
    const baseUrl = hubUrl.replace(/\/+$/, '');
    // Health check
    useEffect(() => {
        if (!enabled)
            return;
        let cancelled = false;
        (async () => {
            try {
                const res = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(5000) });
                if (!cancelled)
                    setHealthy(res.ok);
            }
            catch {
                if (!cancelled)
                    setHealthy(false);
            }
        })();
        return () => { cancelled = true; };
    }, [baseUrl, enabled]);
    // Search / list
    const fetchCatalog = useCallback(async () => {
        if (!enabled || healthy === false)
            return;
        setLoading(true);
        setError(null);
        try {
            let url;
            if (search.trim()) {
                const q = encodeURIComponent(search.trim());
                url = `${baseUrl}/catalog/search?q=${q}&type=mcp_server&limit=50`;
            }
            else {
                url = `${baseUrl}/catalog?type=mcp_server&limit=100&offset=0`;
            }
            const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
            if (!res.ok)
                throw new Error(`HTTP ${res.status}`);
            const data = await res.json();
            // Search returns { items, total }, list returns { items/entities, total }
            const serverItems = data.items || data.entities || [];
            setItems(serverItems);
            setTotal(data.total ?? serverItems.length);
        }
        catch (e) {
            setError(e?.message || 'Failed to reach MatrixHub');
            setItems([]);
            setTotal(0);
        }
        finally {
            setLoading(false);
        }
    }, [baseUrl, enabled, search, healthy]);
    useEffect(() => {
        void fetchCatalog();
    }, [fetchCatalog]);
    // Derived
    const capabilities = useMemo(() => {
        const set = new Set();
        items.forEach((i) => i.capabilities?.forEach((c) => set.add(c)));
        return Array.from(set).sort();
    }, [items]);
    return {
        items,
        total,
        loading,
        error,
        healthy,
        search,
        setSearch,
        capabilities,
        refresh: fetchCatalog,
    };
}