Skip to main content
Glama
PSPDFKit

Nutrient Document Engine MCP Server

by PSPDFKit
Stream.tsx8.63 kB
import React, { createContext, useContext, ReactNode, useState, useEffect, } from "react"; import { useStream } from "@langchain/langgraph-sdk/react"; import { type Message } from "@langchain/langgraph-sdk"; import { uiMessageReducer, type UIMessage, type RemoveUIMessage, } from "@langchain/langgraph-sdk/react-ui"; import { useQueryState } from "nuqs"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { ArrowRight } from "lucide-react"; import { PasswordInput } from "@/components/ui/password-input"; import { getApiKey } from "@/lib/api-key"; import { useThreads } from "./Thread"; import { toast } from "sonner"; import { NutrientLogoWithTitleSVG } from "@/components/icons/nutrient-logo-with-title"; export type StateType = { messages: Message[]; ui?: UIMessage[] }; const useTypedStream = useStream< StateType, { UpdateType: { messages?: Message[] | Message | string; ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage; }; CustomEventType: UIMessage | RemoveUIMessage; } >; type StreamContextType = ReturnType<typeof useTypedStream>; const StreamContext = createContext<StreamContextType | undefined>(undefined); async function sleep(ms = 4000) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function checkGraphStatus( apiUrl: string, apiKey: string | null, ): Promise<boolean> { try { const res = await fetch(`${apiUrl}/info`, { ...(apiKey && { headers: { "X-Api-Key": apiKey, }, }), }); return res.ok; } catch (e) { console.error(e); return false; } } const StreamSession = ({ children, apiKey, apiUrl, assistantId, }: { children: ReactNode; apiKey: string | null; apiUrl: string; assistantId: string; }) => { const [threadId, setThreadId] = useQueryState("threadId"); const { getThreads, setThreads } = useThreads(); const streamValue = useTypedStream({ apiUrl, apiKey: apiKey ?? undefined, assistantId, threadId: threadId ?? null, onCustomEvent: (event, options) => { options.mutate((prev) => { const ui = uiMessageReducer(prev.ui ?? [], event); return { ...prev, ui }; }); }, onThreadId: (id) => { setThreadId(id); // Refetch threads list when thread ID changes. // Wait for some seconds before fetching so we're able to get the new thread that was created. sleep().then(() => getThreads().then(setThreads).catch(console.error)); }, }); useEffect(() => { checkGraphStatus(apiUrl, apiKey).then((ok) => { if (!ok) { toast.error("Failed to connect to LangGraph server", { description: () => ( <p> Please ensure your graph is running at <code>{apiUrl}</code> and your API key is correctly set (if connecting to a deployed graph). </p> ), duration: 10000, richColors: true, closeButton: true, }); } }); }, [apiKey, apiUrl]); return ( <StreamContext.Provider value={streamValue}> {children} </StreamContext.Provider> ); }; // Default values for the form const DEFAULT_API_URL = "http://localhost:2024"; const DEFAULT_ASSISTANT_ID = "agent"; export const StreamProvider: React.FC<{ children: ReactNode }> = ({ children, }) => { // Get environment variables const envApiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL; const envAssistantId: string | undefined = process.env.NEXT_PUBLIC_ASSISTANT_ID; const envApiKey: string | undefined = process.env.NEXT_PUBLIC_LANGSMITH_API_KEY; // Use URL params with env var fallbacks const [apiUrl, setApiUrl] = useQueryState("apiUrl", { defaultValue: envApiUrl || "", }); const [assistantId, setAssistantId] = useQueryState("assistantId", { defaultValue: envAssistantId || "", }); // For API key, use localStorage with env var fallback const [apiKey, _setApiKey] = useState(() => { const storedKey = getApiKey(); return storedKey || envApiKey || ""; }); const setApiKey = (key: string) => { window.localStorage.setItem("lg:chat:apiKey", key); _setApiKey(key); }; // Determine final values to use, prioritizing URL params then env vars const finalApiUrl = apiUrl || envApiUrl; const finalAssistantId = assistantId || envAssistantId; // If we're missing any required values, show the form if (!finalApiUrl || !finalAssistantId) { return ( <div className="flex items-center justify-center min-h-screen w-full p-4"> <div className="animate-in fade-in-0 zoom-in-95 flex flex-col border bg-background shadow-lg rounded-lg max-w-3xl"> <div className="flex flex-col gap-2 mt-14 p-6 border-b"> <div className="flex items-start flex-col gap-2"> <NutrientLogoWithTitleSVG className="h-7" /> </div> <p className="text-muted-foreground"> Welcome to Nutrient Document Chat! Before you get started, you need to enter the URL of the deployment and the assistant / graph ID. </p> </div> <form onSubmit={(e) => { e.preventDefault(); const form = e.target as HTMLFormElement; const formData = new FormData(form); const apiUrl = formData.get("apiUrl") as string; const assistantId = formData.get("assistantId") as string; const apiKey = formData.get("apiKey") as string; setApiUrl(apiUrl); setApiKey(apiKey); setAssistantId(assistantId); form.reset(); }} className="flex flex-col gap-6 p-6 bg-muted/50" > <div className="flex flex-col gap-2"> <Label htmlFor="apiUrl"> Deployment URL<span className="text-rose-500">*</span> </Label> <p className="text-muted-foreground text-sm"> This is the URL of your LangGraph deployment. Can be a local, or production deployment. </p> <Input id="apiUrl" name="apiUrl" className="bg-background" defaultValue={apiUrl || DEFAULT_API_URL} required /> </div> <div className="flex flex-col gap-2"> <Label htmlFor="assistantId"> Assistant / Graph ID<span className="text-rose-500">*</span> </Label> <p className="text-muted-foreground text-sm"> This is the ID of the graph (can be the graph name), or assistant to fetch threads from, and invoke when actions are taken. </p> <Input id="assistantId" name="assistantId" className="bg-background" defaultValue={assistantId || DEFAULT_ASSISTANT_ID} required /> </div> <div className="flex flex-col gap-2"> <Label htmlFor="apiKey">LangSmith API Key</Label> <p className="text-muted-foreground text-sm"> This is <strong>NOT</strong> required if using a local LangGraph server. This value is stored in your browser's local storage and is only used to authenticate requests sent to your LangGraph server. </p> <PasswordInput id="apiKey" name="apiKey" defaultValue={apiKey ?? ""} className="bg-background" placeholder="lsv2_pt_..." /> </div> <div className="flex justify-end mt-2"> <Button type="submit" size="lg"> Continue <ArrowRight className="size-5" /> </Button> </div> </form> </div> </div> ); } return ( <StreamSession apiKey={apiKey} apiUrl={apiUrl} assistantId={assistantId}> {children} </StreamSession> ); }; // Create a custom hook to use the context export const useStreamContext = (): StreamContextType => { const context = useContext(StreamContext); if (context === undefined) { throw new Error("useStreamContext must be used within a StreamProvider"); } return context; }; export default StreamContext;

Latest Blog Posts

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/PSPDFKit/nutrient-document-engine-mcp-server'

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