Skip to main content
Glama
jmandel

Smart EHR MCP Server

by jmandel
AgentTester.tsx6.89 kB
import React, { useState, useEffect, useCallback } from 'react'; import { A2AClient, deepEqual } from '@jmandel/a2a-client/src/A2AClientV2'; import type { Task, Message, Part, Artifact, TaskSendParams } from '@jmandel/a2a-client/src/types'; import './App.css'; const DEFAULT_AGENT_ENDPOINT = 'http://localhost:3001/a2a'; type ClientState = | { phase: 'idle' } | { phase: 'connecting' } | { phase: 'running'; task: Task } | { phase: 'awaiting-input'; task: Task } | { phase: 'completed'; task: Task } | { phase: 'error'; error: any }; function AgentTester() { // ——————————————————— configuration inputs const [agentUrl, setAgentUrl] = useState(DEFAULT_AGENT_ENDPOINT); const [validatedUrl, setValidatedUrl] = useState<string | null>(null); const [agentCard, setAgentCard] = useState<any | null>(null); // ——————————————————— dialog inputs const [kickoffText, setKickoffText] = useState('Start prior auth for MRI Lumbar Spine for low back pain'); const [messageInput, setMessageInput] = useState(''); // ——————————————————— A2A client const [client, setClient] = useState<A2AClient | null>(null); const [state, setState] = useState<ClientState>({ phase: 'idle' }); const [artifacts, setArtifacts] = useState<Artifact[]>([]); // ---------------------- step 1: validate agent (fetch card) const buildCardUrl = (endpoint: string): string => { // Resolve the relative path ".well-known/agent.json" // against the provided endpoint URL as the base. return new URL('.well-known/agent.json', endpoint).toString(); }; const handleValidate = async () => { setAgentCard(null); setValidatedUrl(null); try { const cardUrl = buildCardUrl(agentUrl); const res = await fetch(cardUrl); if (!res.ok) throw new Error(`HTTP ${res.status}`); const card = await res.json(); setAgentCard(card); setValidatedUrl(agentUrl); } catch (e) { alert('Could not fetch agent card: ' + (e as any).message); } }; // ---------------------- step 2: kickoff task const handleKickoff = () => { if (!validatedUrl) return; const params: TaskSendParams = { message: { role: 'user', parts: [{ type: 'text', text: kickoffText }] } }; const c = A2AClient.start(validatedUrl, params, { getAuthHeaders: () => ({}) }); c.on('task-update', (t) => { if (t.status.state === 'input-required') setState({ phase: 'awaiting-input', task: t }); else if (t.status.state === 'completed' || t.status.state === 'failed' || t.status.state === 'canceled') setState({ phase: 'completed', task: t }); else setState({ phase: 'running', task: t }); }); c.on('artifact-update', ({ artifact }) => { setArtifacts((prev) => { const idx = prev.findIndex((a) => a.index === artifact.index); if (idx >= 0) { const next = [...prev]; next[idx] = artifact; return next; } return [...prev, artifact]; }); }); c.on('error', (e) => setState({ phase: 'error', error: e })); c.on('close', () => setClient(null)); setClient(c); setArtifacts([]); setState({ phase: 'connecting' }); }; // ---------------------- send follow-up message const handleSend = () => { if (!client || state.phase !== 'awaiting-input') return; const msg: Message = { role: 'user', parts: [{ type: 'text', text: messageInput }] }; client.send(msg); setMessageInput(''); }; const handleCancel = () => { client?.cancel(); }; const currentTask = state.phase === 'running' || state.phase === 'awaiting-input' || state.phase === 'completed' ? state.task : null; // helper render artifact parts const renderPart = (part: Part, idx: number) => { if (part.type === 'text') return <p key={idx}>{part.text}</p>; if (part.type === 'data') return <pre key={idx}>{JSON.stringify(part.data, null, 2)}</pre>; if (part.type === 'file') return <p key={idx}>📎 File part ({part.file?.mimeType})</p>; return <span key={idx}>Unknown part</span>; }; return ( <div className="App"> <h1>A2A Agent Tester (Raw Client)</h1> {/* Agent selection */} <div className="card"> <h2>1. Select Agent</h2> <input style={{ width: '400px' }} value={agentUrl} onChange={(e) => setAgentUrl(e.target.value)} /> <button onClick={handleValidate}>Fetch Agent Card</button> {agentCard && ( <p style={{ marginTop: '8px' }}> ✅ {agentCard.name} (v{agentCard.version}) – streaming:{' '} {agentCard.capabilities?.streaming ? 'yes' : 'no'} </p> )} </div> {/* Kickoff */} {agentCard && ( <div className="card"> <h2>2. Kick-off Task</h2> <input style={{ width: '600px' }} value={kickoffText} onChange={(e) => setKickoffText(e.target.value)} /> <button onClick={handleKickoff} disabled={!!client}>Start</button> </div> )} {/* Conversation */} {currentTask && ( <div className="card"> <h2>Conversation</h2> <div style={{ maxHeight: 300, overflowY: 'auto', border: '1px solid #ddd', padding: 8 }}> {currentTask.history?.map((m, i) => ( <div key={i} style={{ marginBottom: 6 }}> <strong>{m.role === 'user' ? '🧑‍💻' : '🤖'} </strong> {m.parts.map((p, idx) => ( <span key={idx}>{renderPart(p, idx)}</span> ))} </div> ))} </div> {state.phase === 'awaiting-input' && ( <div style={{ marginTop: 10 }}> <input style={{ width: 400 }} value={messageInput} onChange={(e) => setMessageInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSend()} /> <button onClick={handleSend}>Send</button> </div> )} <button style={{ marginTop: 10 }} onClick={handleCancel} disabled={!client}> Cancel Task </button> </div> )} {/* Artifacts */} {artifacts.length > 0 && ( <div className="card"> <h2>Artifacts</h2> {artifacts.map((a) => ( <div key={a.index} style={{ borderBottom: '1px solid #eee', marginBottom: 6 }}> <h4> #{a.index} {a.name} </h4> {a.parts.map((p, idx) => renderPart(p, idx))} </div> ))} </div> )} {state.phase === 'error' && <pre style={{ color: 'red' }}>{JSON.stringify(state.error)}</pre>} </div> ); } export default AgentTester;

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