Skip to main content
Glama

Convex MCP server

Official
by get-convex
DebugView.tsx14.2 kB
import { useCallback, useState } from "react"; import { IndexRangeBounds, PaginatorSubscriptionId } from "../shared/types"; // import { SyncQueryManager } from "../local-store/syncQueryManager"; import { useMutation } from "convex/react"; import { LocalDbReaderImpl } from "../browser/localDbReader"; // import { SchemaView } from "./SchemaView"; import { PageTimeline } from "./PageTimeline"; import { TableViewer } from "./TableView"; import { anyApi } from "convex/server"; import { LocalStoreClient } from "../browser/ui"; interface DebugViewProps { onClose: () => void; } function renderBounds(bounds: IndexRangeBounds) { return ( <span className="text-gray-500"> <span className="text-gray-900 px-1 font-bold"> {bounds.lowerBoundInclusive ? "[" : "("} </span> <span className="text-gray-500"> {bounds.lowerBound.length > 0 ? JSON.stringify(bounds.lowerBound) : "-∞"} </span> <span className="text-gray-900 px-1 font-bold">{"→"}</span> <span className="text-gray-500"> {bounds.upperBound.length > 0 ? JSON.stringify(bounds.upperBound) : "∞"} </span> <span className="text-gray-900 px-1 font-bold"> {bounds.upperBoundInclusive ? "]" : ")"} </span> </span> ); } type DebugTab = | "schema" | "tables" | "documents" | "mutations" | "queries" | "indexeddb"; function TabContent({ tab }: { tab: DebugTab }) { const _syncQueryManager: LocalStoreClient = (globalThis as any).localDb; if (tab === "schema") { return ( <div className="overflow-auto max-h-[600px]"> {/* <SchemaView schemaString={(syncQueryManager.syncSchema as any).export()} /> */} </div> ); } if (tab === "tables") { return ( <div className="overflow-auto max-h-[600px]"> <TableViewer /> </div> ); } if (tab === "indexeddb") { return ( <div className="overflow-auto max-h-[600px]"> <IndexedDBViewer /> </div> ); } if (tab === "queries") { return ( <div className="overflow-auto max-h-[600px]"> <QueryViewer /> </div> ); } return ( <div className="flex items-center justify-center h-64 text-gray-500"> Coming soon... </div> ); } function IndexedDBViewer() { const _syncQueryManager: LocalStoreClient = (globalThis as any).localDb; const [pages, _setPages] = useState<any[]>([]); const [selectedPage, setSelectedPage] = useState<any>(null); const [expandedDocs, setExpandedDocs] = useState<Set<string>>(new Set()); // TODO -- restore this // const loadPages = async () => { // const allPages = // await syncQueryManager.clientStore.serverStore.indexedDbClient.db // .table("pages") // .toArray(); // setPages(allPages); // }; // useEffect(() => { // void loadPages(); // }, [syncQueryManager]); const toggleDocument = (id: string) => { const newExpanded = new Set(expandedDocs); if (newExpanded.has(id)) { newExpanded.delete(id); } else { newExpanded.add(id); } setExpandedDocs(newExpanded); }; const handleDeleteDB = async () => { if ( window.confirm( "Are you sure you want to delete the IndexedDB? This action cannot be undone.", ) ) { // TODO -- restore this // await syncQueryManager.clientStore.serverStore.indexedDbClient.db.delete(); window.location.reload(); // Reload the page to reinitialize the DB } }; const clearAll = useMutation(anyApi.sync.misc.clearAll); const handleClearAll = async () => { if ( window.confirm( "Are you sure you want to clear all data? This action cannot be undone.", ) ) { await clearAll(); // TODO: We shouldn't need to do this. // await syncQueryManager.clientStore.serverStore.indexedDbClient.db.delete(); window.location.reload(); // Reload the page to reinitialize the DB } }; return ( <div className="grid grid-cols-3 gap-8 h-[600px]"> {/* Left Column - Pages View */} <div className="col-span-2"> <h3 className="font-medium text-gray-900 mb-4">Stored Pages</h3> <div className="overflow-auto pr-2 h-[calc(100%-2rem)]"> <div className="grid grid-cols-1 gap-4"> {pages.map((page, i) => ( <div key={i} className="border rounded-lg p-4"> <button onClick={() => setSelectedPage(selectedPage === page ? null : page) } className="flex items-center gap-2 w-full text-left" > <span className="transform transition-transform duration-200" style={{ display: "inline-block", transform: `rotate(${ selectedPage === page ? "90deg" : "0deg" })`, }} > ▶ </span> <div> <div className="font-medium"> {page.table} / {page.index} </div> <div className="text-sm text-gray-500"> {page.documents.length} documents </div> </div> </button> {selectedPage === page && ( <div className="mt-4 pl-6 space-y-4"> <div className="space-y-1"> <div> <span className="text-gray-500">Lower Bound:</span>{" "} {JSON.stringify(page.lowerBound)} </div> <div> <span className="text-gray-500">Upper Bound:</span>{" "} {JSON.stringify(page.upperBound)} </div> </div> <div> {page.documents.map((doc: any) => ( <div key={doc._id} className="space-y-1"> <button onClick={() => toggleDocument(doc._id)} className="flex items-center gap-1 text-gray-600 hover:text-gray-900" > <span className="transform transition-transform duration-200" style={{ display: "inline-block", transform: `rotate(${ expandedDocs.has(doc._id) ? "90deg" : "0deg" })`, }} > ▶ </span> {doc._id} </button> {expandedDocs.has(doc._id) && ( <pre className="pl-4 text-xs bg-white p-2 rounded border border-gray-200 overflow-auto"> {JSON.stringify(doc, null, 2)} </pre> )} </div> ))} </div> </div> )} </div> ))} </div> </div> </div> {/* Right Column - Actions */} <div> <h3 className="font-medium text-gray-900 mb-4">Actions</h3> <div className="border rounded-lg p-4 bg-red-50 space-y-4"> <h4 className="font-medium text-red-900">Danger Zone</h4> <p className="text-sm text-red-700"> Deleting the IndexedDB will remove all cached data. The application will need to reload and refetch data from the server. </p> <button onClick={() => { void handleDeleteDB(); }} className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors text-sm font-medium focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2" > Delete IndexedDB </button> <p className="text-sm text-red-700"> Clear all data from the server and also delete the IndexedDB. </p> <button onClick={() => void handleClearAll()} className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors text-sm font-medium focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2" > Clear All Data </button> </div> </div> </div> ); } function QueryViewer() { const queries: Map<string, LocalDbReaderImpl> = (globalThis as any).debugSyncQueries ?? new Map(); return ( <div className="space-y-6"> {Array.from(queries.entries()).map(([id, query]) => ( <div key={id} className="border rounded-lg p-4"> <h3 className="text-lg font-semibold text-gray-800 mb-2"> {id.startsWith("random:") ? "useSyncQuery" : id} </h3> <div className="pl-4"> <SingleQueryView query={query} /> </div> </div> ))} </div> ); } function SingleQueryView({ query }: { query: LocalDbReaderImpl }) { const indexRanges = Array.from(query.debugIndexRanges.entries()); return ( <div className="space-y-4"> {indexRanges.map(([id, indexRange]) => ( <IndexRangeView id={id as PaginatorSubscriptionId} indexRange={indexRange} /> ))} </div> ); } function IndexRangeView({ indexRange, id, }: { id: PaginatorSubscriptionId; indexRange: { table: string; index: string; indexRangeBounds: IndexRangeBounds; order: "asc" | "desc"; limit: number; }; }) { const syncQueryManager: LocalStoreClient = (globalThis as any).localDb; // TODO -- restore this const orderedPages: any[] = []; // const indexPaginator = syncQueryManager.coreLocalStore.paginator.getIndexPaginator( // indexRange.table, // indexRange.index, // ); // const paginatorSubscription = indexPaginator.subscriptions.get(id)!; // const pageSubscriptionIds = paginatorSubscription.pageSubscriptionIds; // const fulfilled = indexPaginator.tryFulfillSingleSubscription( // paginatorSubscription, // ); // if (fulfilled.state !== "fulfilled") { // return <div>Waiting on loading page</div>; // } // // NOTE there's also fulfilled.pageSubscriptionIds which should be the same. // const orderedPages = // syncQueryManager.clientStore.serverStore.orderedPageResults( // pageSubscriptionIds, // ); return ( <div key={id} className="bg-white rounded-lg border border-gray-200 p-4"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-1"> <span className="font-medium">Index Range Subscription Id:</span> <code className="bg-gray-100 px-1 py-0.5 rounded">{id}</code> </div> <div className="flex items-center gap-2 text-sm"> <span className="font-medium text-gray-700">{indexRange.table}</span> <span className="text-gray-700">.</span> <span className="font-medium text-gray-700">{indexRange.index}</span> </div> <div className="text-xs text-gray-500"> <div>Range: {renderBounds(indexRange.indexRangeBounds)}</div> <div>Order: {indexRange.order}</div> <div> Limit:{" "} {indexRange.limit === Number.POSITIVE_INFINITY ? "∞" : indexRange.limit} </div> <PageTimeline syncSchema={syncQueryManager.syncSchema} orderedPages={orderedPages} rangeBounds={indexRange.indexRangeBounds} /> </div> </div> ); } export function DebugView({ onClose }: DebugViewProps) { const [activeTab, setActiveTab] = useState<DebugTab>("schema"); const handleBackdropClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }, [onClose], ); return ( <div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, display: "flex", alignItems: "start", justifyContent: "center", overflow: "auto", padding: "8px", zIndex: 50, }} onClick={handleBackdropClick} > <div style={{ backgroundColor: "white", maxHeight: "calc(100vh - 16px)", marginTop: "8px", marginBottom: "8px", marginLeft: "auto", marginRight: "auto", padding: "16px", width: "100vw", }} > <h1 className="text-2xl font-bold mb-6 text-gray-800">Debug View</h1> {/* Tabs */} <div className="border-b border-gray-200 mb-6"> <nav className="-mb-px flex space-x-8"> {[ { id: "schema", label: "Sync Schema" }, { id: "tables", label: "Table Viewer" }, { id: "documents", label: "Documents" }, { id: "mutations", label: "Mutations" }, { id: "queries", label: "Local Sync Queries" }, { id: "indexeddb", label: "IndexedDB" }, ].map((tab) => ( <button key={tab.id} onClick={() => setActiveTab(tab.id as DebugTab)} className={` py-2 px-1 border-b-2 font-medium text-sm ${ activeTab === tab.id ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" } `} > {tab.label} </button> ))} </nav> </div> {/* Tab Content */} <TabContent tab={activeTab} /> </div> </div> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server