Skip to main content
Glama
jmandel

Smart EHR MCP Server

by jmandel
main.ts13.3 kB
import { A2AClient, A2AClientConfig, ClientManagedState, TaskUpdatePayload, StatusUpdatePayload, ErrorPayload, ClosePayload, ClientCloseReason } from '../src/A2AClient'; import { Task, Message, TextPart, TaskSendParams, JsonRpcError, TaskState } from '../src/types'; // --- Constants --- const JOKE_AGENT_URL = 'http://localhost:3100/a2a'; const LOCAL_STORAGE_TASK_ID_KEY = 'a4aDemoTaskId'; const LOCAL_STORAGE_AGENT_URL_KEY = 'a4aDemoAgentUrl'; // --- DOM Elements --- const startButton = document.getElementById('start-joke-button') as HTMLButtonElement; const resetButton = document.getElementById('reset-button') as HTMLButtonElement; const statusArea = document.getElementById('status-area') as HTMLDivElement; const taskDetailsArea = document.getElementById('task-details-area') as HTMLDivElement; const errorArea = document.getElementById('error-area') as HTMLDivElement; // --- Application State --- let currentClient: A2AClient | null = null; let currentTask: Task | null = null; let clientState: ClientManagedState = 'idle'; let lastError: Error | JsonRpcError | null = null; let lastCloseReason: ClientCloseReason | null = null; // --- A2AClient Configuration --- // We omit agentEndpointUrl here because it's passed directly to create/resume const clientConfig: Omit<A2AClientConfig, 'agentEndpointUrl'> = { getAuthHeaders: async () => ({}), // No auth needed for joke agent pollIntervalMs: 1000, // Adjust as needed // Let agent card decide strategy (SSE or poll) }; // --- UI Update Function --- function updateUI() { statusArea.textContent = `Client State: ${clientState}`; // Task Details if (currentTask) { let taskText = `Task ID: ${currentTask.id}\n`; taskText += `Agent State: ${currentTask.status.state}\n`; // Show agent message ONLY if not awaiting input (handled by prompt logic elsewhere) if (clientState !== 'input-required' && currentTask.status.message) { taskText += `Agent Message: ${(currentTask.status.message.parts[0] as TextPart)?.text ?? '(No text)'}\n`; } if (currentTask.artifacts && currentTask.artifacts.length > 0) { taskText += `\nArtifacts:\n`; currentTask.artifacts.forEach((art, index) => { taskText += ` [${index}] ${art.name || 'N/A'}:`; art.parts.forEach(part => { if (part.type === 'text') taskText += ` "${part.text}"`; else taskText += ` [${part.type}]`; }); taskText += '\n'; }); } taskDetailsArea.textContent = taskText; } else { taskDetailsArea.textContent = '(No active task)'; } // Error Area if (lastError) { let errorText = `Error: ${lastError.message}\n`; if ('code' in lastError && lastError.code) { errorText += `Code: ${lastError.code}\n`; } if ('data' in lastError && lastError.data) { errorText += `Data: ${JSON.stringify(lastError.data)}\n`; } errorArea.textContent = errorText; errorArea.style.display = 'block'; } else if (clientState === 'closed' && lastCloseReason && !['task-completed', 'task-canceled-by-client', 'task-canceled-by-agent'].includes(lastCloseReason)) { // Show non-standard close reasons as errors errorArea.textContent = `Client Closed Unexpectedly: ${lastCloseReason}`; errorArea.style.display = 'block'; } else { errorArea.style.display = 'none'; } // Button State (Disabled unless idle, closed, or error) startButton.disabled = !['idle', 'closed', 'error'].includes(clientState); } // --- Event Handlers --- async function handleInputRequired(task: Task) { console.log("handleInputRequired called"); const promptMessage = task.status.message; if (!promptMessage) { console.warn("input-required state but no prompt message found."); lastError = new Error("Agent requires input but didn't provide a prompt."); updateUI(); return; } const agentPromptPart = promptMessage.parts.find(p => p.type === 'text') as TextPart | undefined; const promptText = agentPromptPart?.text ?? "The agent needs input (provide topic):"; // Use timeout to allow UI to update before blocking with prompt setTimeout(async () => { console.log("Prompting user..."); const userInput = window.prompt(promptText); // Re-check state *after* prompt if (!currentClient || currentClient.getCurrentState() !== 'input-required') { console.warn("Client state changed while prompt was open. Input ignored."); return; } if (userInput === null) { console.log("User cancelled the prompt."); // Optional: Cancel the task if prompt is cancelled // await currentClient.cancel(); } else { console.log(`User provided topic: "${userInput}"`); const responseMessage: Message = { role: 'user', parts: [{ type: 'text', text: userInput }] }; try { await currentClient.send(responseMessage); clientState = currentClient.getCurrentState(); // Should be 'sending' or similar briefly lastError = null; // Clear previous errors updateUI(); } catch (err) { console.error("Error calling client.send:", err); lastError = err instanceof Error ? err : new Error(String(err)); clientState = currentClient.getCurrentState(); // Update state even on error updateUI(); } } }, 50); // Small delay } function handleTaskUpdate(payload: TaskUpdatePayload) { console.log("Received task-update:", payload); currentTask = payload.task; // Update the general clientState variable for UI purposes // Status-specific reactions are handled in handleStatusUpdate clientState = currentClient?.getCurrentState() ?? clientState; lastError = null; // Clear error on successful update updateUI(); } // Added: Handler specifically for status changes function handleStatusUpdate(payload: StatusUpdatePayload) { console.log("Received status-update:", payload.status); // Update the clientState variable whenever status changes clientState = currentClient?.getCurrentState() ?? clientState; // React specifically to input-required state if (payload.status.state === 'input-required') { // Pass the task object from the payload, which includes the necessary message handleInputRequired(payload.task); } // Potentially handle other status-specific logic here if needed updateUI(); // Update UI based on potential clientState change } function handleError(payload: ErrorPayload) { console.error("Received client error:", payload); lastError = payload.error; clientState = currentClient?.getCurrentState() ?? 'error'; // Client might already be in error state updateUI(); } function handleClose(payload: ClosePayload) { console.log(`Received client close: ${payload.reason}`); clientState = 'closed'; lastCloseReason = payload.reason; // Don't clear task display, keep the last known state currentClient = null; // Important: Release the client instance updateUI(); } // --- Task Management Functions --- function storeTask(taskId: string, agentUrl: string) { try { localStorage.setItem(LOCAL_STORAGE_TASK_ID_KEY, taskId); localStorage.setItem(LOCAL_STORAGE_AGENT_URL_KEY, agentUrl); console.log(`Stored task ${taskId} for agent ${agentUrl}`); } catch (e) { console.error("Failed to store task in localStorage:", e); } } function clearStoredTask() { try { localStorage.removeItem(LOCAL_STORAGE_TASK_ID_KEY); localStorage.removeItem(LOCAL_STORAGE_AGENT_URL_KEY); console.log("Cleared stored task from localStorage."); } catch (e) { console.error("Failed to clear task from localStorage:", e); } } function getStoredTask(): { taskId: string; agentUrl: string } | null { try { const taskId = localStorage.getItem(LOCAL_STORAGE_TASK_ID_KEY); const agentUrl = localStorage.getItem(LOCAL_STORAGE_AGENT_URL_KEY); if (taskId && agentUrl) { return { taskId, agentUrl }; } } catch (e) { console.error("Failed to read task from localStorage:", e); } return null; } // --- Start/Resume Task Functions --- function registerClientListeners(client: A2AClient) { client.on('task-update', handleTaskUpdate); client.on('status-update', handleStatusUpdate); client.on('error', handleError); client.on('close', handleClose); } async function startJokeTask() { if (currentClient) { console.warn("Task already in progress."); return; } console.log("Starting new jokeAboutTopic task..."); currentTask = null; lastError = null; lastCloseReason = null; clientState = 'initializing'; updateUI(); // 1. Prepare initial parameters and determine task ID let startParams: TaskSendParams = { message: { role: 'user', parts: [{ type: 'text', text: 'Tell me a joke about...' }] }, metadata: { skillId: 'jokeAboutTopic' } // id can be pre-populated or generated below }; const taskId = startParams.id ?? crypto.randomUUID(); startParams = { ...startParams, id: taskId }; // Ensure ID is in params // 2. Store task details immediately storeTask(taskId, JOKE_AGENT_URL); // 3. Attempt to create the client try { currentClient = await A2AClient.create(JOKE_AGENT_URL, startParams, clientConfig); console.log("Client created, Task ID:", currentClient.taskId); // Task details already stored registerClientListeners(currentClient); clientState = currentClient.getCurrentState(); currentTask = currentClient.getCurrentTask(); if (clientState === 'input-required' && currentTask) { handleInputRequired(currentTask); } updateUI(); } catch (error) { console.error("Error during A2AClient.create:", error); lastError = error instanceof Error ? error : new Error(String(error)); clientState = 'error'; currentClient = null; updateUI(); } } async function resumeJokeTask(taskId: string, agentUrl: string) { if (currentClient) { console.warn("Task already in progress."); return; } console.log(`Attempting to resume task ${taskId} at ${agentUrl}...`); currentTask = null; lastError = null; lastCloseReason = null; clientState = 'initializing'; // Or 'resuming'? Let's use initializing for simplicity updateUI(); try { currentClient = await A2AClient.resume(agentUrl, taskId, clientConfig); console.log("Client resumed, Task ID:", currentClient.taskId); // No need to store again, already stored registerClientListeners(currentClient); clientState = currentClient.getCurrentState(); currentTask = currentClient.getCurrentTask(); if (clientState === 'input-required' && currentTask) { handleInputRequired(currentTask); } else if (clientState === 'error') { // If resume immediately results in error state, capture it lastError = new Error("Client entered error state immediately after resume."); } updateUI(); } catch (error) { console.error("Error during A2AClient.resume:", error); lastError = error instanceof Error ? error : new Error(String(error)); clientState = 'error'; currentClient = null; updateUI(); } } // --- Reset Function --- function handleReset() { console.log("Reset button clicked."); // 1. Clear stored task details clearStoredTask(); // 2. Close any active client connection if (currentClient) { console.log("Closing active client connection..."); // Use 'closed-by-caller' or a specific reset reason currentClient.close('closed-by-caller'); // Note: The client's 'close' event will fire, setting state to 'closed' // We will reset state manually below anyway. } // 3. Reset application state variables currentClient = null; currentTask = null; lastError = null; lastCloseReason = null; clientState = 'idle'; // Set state directly to idle // 4. Update UI console.log("Reset complete. Updating UI."); updateUI(); } // --- Initialization --- function initializeApp() { const storedTask = getStoredTask(); if (storedTask) { console.log(`Found stored task: ${storedTask.taskId}`); resumeJokeTask(storedTask.taskId, storedTask.agentUrl); } else { console.log("No stored task found."); updateUI(); // Show initial idle state } startButton.addEventListener('click', startJokeTask); resetButton.addEventListener('click', handleReset); console.log("Direct A2A Client Demo initialized."); } initializeApp(); // Run initialization logic

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/jmandel/health-record-mcp'

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