MCP UI का निर्माण: AI एजेंट्स के लिए सर्वर-संचालित इंटरैक्टिव विजेट्स
Model Context Protocol (MCP) पर संरचित JSON स्कीमा, क्लाइंट-साइड सैंडबॉक्स्ड रेंडरिंग और द्वि-दिशात्मक एक्शन लूप के साथ सर्वर-संचालित जनरेटिव UI की वास्तुकला।

Model Context Protocol (MCP) ने AI एजेंट्स के लिए एक ज़रूरी मानक तैयार किया: JSON-RPC 2.0 के ज़रिए मॉडल्स को डेटाबेस, स्थानीय CLI टूल्स और रिमोट APIs से जोड़ने का एक साफ़-सुथरा तरीक़ा। हर LLM प्रोवाइडर के लिए अलग से कस्टम टूल ब्रिज बनाने का झंझट ख़त्म हुआ।
लेकिन इसमें एक साफ़ रुकावट थी: डिफ़ॉल्ट MCP टूल कॉल्स केवल सादा टेक्स्ट या कच्चा JSON स्ट्रिंग लौटाते हैं।
जब कोई एजेंट आंतरिक नॉलेज बेस से डेटा खोजता है, परिनियोजन (डिप्लॉयमेंट) ट्रेसेस का विश्लेषण करता है या कोई डेटाबेस क्वेरी करता है, तो चैट विंडो में लंबा टेक्स्ट फेंकना बहुत ख़राब यूज़र अनुभव है। आप एक क्लिक में रिकॉर्ड्स फ़िल्टर नहीं कर सकते, कोई डिटेल ड्रॉर नहीं खोल सकते, और अगली कार्रवाई के लिए आपको दोबारा पूरा वाक्य लिखना पड़ता है ताकि मॉडल पैरामीटर्स का अंदाज़ा लगा सके।
MCP UI इस समस्या को Server-Driven UI (SDUI) और MCP सर्वर्स के तालमेल से सुलझाता है। टेक्स्ट डंप के बजाय सर्वर टाइप किए गए कंपोनेंट स्कीमा और एक्शन पेलोड लौटाता है। फ्रंटएंड इन्हें सीधे इंटरैक्टिव React विजेट्स के रूप में रेंडर करता है और एजेंट रनटाइम के साथ सीधा फ़ीडबैक लूप बनाता है।
आइए इस पूरे आर्किटेक्चर को विस्तार से समझें: वायर प्रोटोकॉल डिज़ाइन और TypeScript सर्वर्स से लेकर सुरक्षित React होस्ट्स और आइसोलेशन बाउंड्रीज़ तक।
आर्किटेक्चर: MCP UI का त्रि-स्तरीय इवेंट लूप
सामान्य MCP सेटअप में क्लाइंट होस्ट केवल LLM और MCP सर्वर के बीच स्ट्रिंग्स पास करने का एक पैसिव ब्रिज होता है।
MCP UI के साथ होस्ट दो नई ज़िम्मेदारियाँ संभालता है: एक Component Registry (जो कंपोनेंट IDs को सुरक्षित React कंपोनेंट्स से जोड़ती है) और एक Action Dispatcher (जो विजेट क्लिक्स को बिना दोबारा LLM कॉल किए सीधे टूल से जोड़ता है)।
┌──────────────────────────────────────────────────────────────────┐
│ AI Agent Client Host (Next.js) │
│ │
│ ┌────────────────────┐ ┌───────────────────────────────┐ │
│ │ LLM Orchestrator │ │ Dynamic UI Renderer │ │
│ │ (Vercel AI SDK) │ │ │ │
│ └─────────┬──────────┘ │ ┌─────────────────────────┐ │ │
│ │ │ │ Component Cache │ │ │
│ │ tools/call │ └────────────┬────────────┘ │ │
│ ▼ │ ▼ │ │
│ ┌────────────────────┐ │ [ BlogDiscoveryCard ] │ │
│ │ MCP Client Layer │──────►│ [ Action Buttons ] │ │
│ │ (JSON-RPC Router) │◄──────│ │ │ │
│ └─────────┬──────────┘ └───────────────┼───────────────┘ │
│ │ │ │
│ │ JSON-RPC 2.0 (Stdio / SSE) │ │
└────────────┼──────────────────────────────────┼──────────────────┘
│ │
▼ │ UI Action Callback
┌────────────────────────────────────────┐ │ (tools/call: on_select)
│ Custom MCP Server │ │
│ │ │
│ ┌──────────────────────────────────┐ │ │
│ │ Tool: discover_knowledge_node │ │ │
│ │ ├─ Query Vector / Metadata Store │ │ │
│ │ └─ Generate UI Schema Definition │◄─┴──────┘
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘यह जीवन चक्र पाँच चरणों में आगे बढ़ता है:
- इरादा निष्पादन: एजेंट खोज मापदंडों या इकाई पहचानकर्ताओं के साथ एक MCP टूल (
discover_knowledge_node) को ट्रिगर करता है। - दोहरा पेलोड सीरियलाइजेशन: MCP सर्वर दो पेलोड उत्पन्न करता है: टर्मिनल क्लाइंट्स के लिए सादा टेक्स्ट मार्कडाउन फ़ॉलबैक और
application/vnd.mcp.ui+jsonके अनुरूप संरचित JSON स्कीमा। - रजिस्ट्री रिज़ॉल्यूशन: फ्रंटएंड होस्ट टूल परिणाम प्राप्त करता है, Zod के साथ स्कीमा हस्ताक्षर को मान्य करता है, और स्थानीय घटक रजिस्ट्री से घटक को हल करता है।
- सैंडबॉक्स्ड माउंट: होस्ट घटक को माउंट करता है, प्रॉप्स इंजेक्ट करता है और कॉलबैक हैंडलर को जोड़ता है।
- द्वि-दिशात्मक कॉलबैक: जब उपयोगकर्ता किसी एक्शन बटन पर क्लिक करता है, तो घटक MCP क्लाइंट परत के माध्यम से एक्शन वापस भेजता है, जिससे बिना नए टेक्स्ट प्रॉम्प्ट के टूल निष्पादित होता है।
Wire Specification: The MCP UI Protocol Contract
Model Context Protocol विनिर्देश के साथ पूर्ण पिछड़ा संगतता बनाए रखने के लिए, UI पेलोड को संरचित संसाधन प्रतिनिधित्व का उपयोग करके मानक MCP टूल कॉल परिणामों के भीतर समाहित किया जाता है।
The JSON-RPC 2.0 Wire Format
जब कोई MCP टूल एक इंटरैक्टिव UI घटक उत्पन्न करता है, तो यह content सरणी को एक टाइप किए गए UI परिभाषा से भरता है:
{
"jsonrpc": "2.0",
"id": "req-9841",
"result": {
"content": [
{
"type": "text",
"text": "Found 1 matching article: 'Building MCP UI: Server-Driven Interactive Widgets for AI Agents'."
},
{
"type": "resource",
"resource": {
"uri": "ui://components/blog-discovery-card/mcp-ui-server-driven-interactive-components",
"mimeType": "application/vnd.mcp.ui+json",
"text": "{\"component\":\"BlogDiscoveryCard\",\"version\":\"1.0.0\",\"props\":{\"title\":\"Building MCP UI: Server-Driven Interactive Widgets for AI Agents\",\"slug\":\"mcp-ui-server-driven-interactive-components\",\"description\":\"Architecting server-driven generative UI over Model Context Protocol (MCP)...\",\"readingTime\":8,\"tags\":[\"MCP\",\"AI Agents\",\"Server-Driven UI\"],\"coverImage\":\"/images/blog/mcp-ui-server-driven-interactive-components-cover.png\"},\"actions\":[{\"id\":\"filter_tag\",\"label\":\"Filter by Tag\",\"tool\":\"discover_knowledge_node\",\"parameters\":{\"tag\":\"AI Agents\"}},{\"id\":\"preview_section\",\"label\":\"Read Abstract\",\"tool\":\"get_article_abstract\",\"parameters\":{\"slug\":\"mcp-ui-server-driven-interactive-components\"}}]}"
}
}
]
}
}TypeScript UI Schema Definition
Zod का उपयोग करके, हम सर्वर द्वारा उत्सर्जित सभी UI घटकों के लिए सख्त संरचनात्मक अनुबंध परिभाषित करते हैं:
import { z } from "zod";
export const McpUiActionSchema = z.object({
id: z.string(),
label: z.string(),
tool: z.string(),
parameters: z.record(z.unknown()),
variant: z.enum(["primary", "secondary", "danger"]).default("secondary"),
});
export const McpUiPayloadSchema = z.object({
component: z.string(),
version: z.string(),
props: z.record(z.unknown()),
actions: z.array(McpUiActionSchema).default([]),
});
export type McpUiAction = z.infer<typeof McpUiActionSchema>;
export type McpUiPayload = z.infer<typeof McpUiPayloadSchema>;Server-Side Implementation: Exposing MCP UI in TypeScript
आइए @modelcontextprotocol/sdk का उपयोग करके एक प्रोडक्शन-रेडी Node.js MCP सर्वर लागू करें। यह सर्वर discover_knowledge_node टूल को उजागर करता है, जो पढ़ने योग्य टेक्स्ट और संरचित BlogDiscoveryCard घटक पेलोड दोनों लौटाता है।
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
interface KnowledgeArticle {
title: string;
slug: string;
description: string;
readingTime: number;
tags: string[];
coverImage: string;
}
const KNOWLEDGE_BASE: Record<string, KnowledgeArticle> = {
"mcp-ui-server-driven-interactive-components": {
title: "Building MCP UI: Server-Driven Interactive Widgets for AI Agents",
slug: "mcp-ui-server-driven-interactive-components",
description: "Architecting server-driven generative UI over Model Context Protocol using JSON schemas and sandboxed rendering.",
readingTime: 8,
tags: ["MCP", "AI Agents", "Server-Driven UI", "React"],
coverImage: "/images/blog/mcp-ui-server-driven-interactive-components-cover.png",
},
};
const server = new Server(
{
name: "knowledge-mcp-ui-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Register Tool Discovery
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "discover_knowledge_node",
description: "Search technical journal articles and return rich interactive cards.",
inputSchema: {
type: "object",
properties: {
topic: { type: "string", description: "Search query or keyword" },
tag: { type: "string", description: "Optional category tag filter" },
},
required: ["topic"],
},
},
],
};
});
// Handle Tool Execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== "discover_knowledge_node") {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const args = request.params.arguments as { topic: string; tag?: string };
const matchedKey = Object.keys(KNOWLEDGE_BASE).find((key) =>
key.includes(args.topic.toLowerCase().replace(/\s+/g, "-"))
) || "mcp-ui-server-driven-interactive-components";
const article = KNOWLEDGE_BASE[matchedKey];
// Construct UI Component Payload
const uiPayload = {
component: "BlogDiscoveryCard",
version: "1.0.0",
props: {
title: article.title,
slug: article.slug,
description: article.description,
readingTime: article.readingTime,
tags: article.tags,
coverImage: article.coverImage,
},
actions: [
{
id: "filter_by_tag",
label: `More on ${article.tags[0]}`,
tool: "discover_knowledge_node",
parameters: { topic: article.tags[0], tag: article.tags[0] },
variant: "secondary" as const,
},
{
id: "open_reader",
label: "Open Full Post",
tool: "navigate_route",
parameters: { href: `/en/journal/${article.slug}` },
variant: "primary" as const,
},
],
};
return {
content: [
{
type: "text",
text: `Found article: "${article.title}" (${article.readingTime} min read).`,
},
{
type: "resource",
resource: {
uri: `ui://knowledge/${article.slug}`,
mimeType: "application/vnd.mcp.ui+json",
text: JSON.stringify(uiPayload),
},
},
],
};
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
process.stderr.write(`Server startup failed: ${err.message}\n`);
process.exit(1);
});Client-Side Host: Dynamic Component Registry & Action Bridge
फ्रंटएंड पर (उदाहरण के लिए Next.js App Router एप्लिकेशन के भीतर), हम एक होस्ट रेंडरर बनाते हैं जो:
- MCP टूल प्रतिक्रियाओं को पार्स करता है और
application/vnd.mcp.ui+jsonसंसाधनों की जांच करता है। - घटक नाम को एक दृढ़ता से टाइप की गई React घटक रजिस्ट्री से मिलाता है।
- उपयोगकर्ता इंटरैक्शन इवेंट्स को सीधे MCP ट्रांसपोर्ट चैनल पर वापस भेजता है।
The React Component Registry
import React from "react";
import Image from "next/image";
import { McpUiAction } from "./types";
interface BlogDiscoveryCardProps {
title: string;
slug: string;
description: string;
readingTime: number;
tags: string[];
coverImage: string;
actions: McpUiAction[];
onAction: (action: McpUiAction) => void;
}
export function BlogDiscoveryCard({
title,
description,
readingTime,
tags,
coverImage,
actions,
onAction,
}: BlogDiscoveryCardProps) {
return (
<div style={{ border: "1px solid var(--border-color, #e2e8f0)", borderRadius: 8, padding: 16, background: "var(--surface, #ffffff)" }}>
{coverImage && (
<div style={{ position: "relative", width: "100%", height: 160, marginBottom: 12 }}>
<Image
src={coverImage}
alt={title}
fill
sizes="(max-width: 768px) 100vw, 400px"
style={{ objectFit: "cover", borderRadius: 4 }}
/>
</div>
)}
<div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
{tags.map((tag) => (
<span key={tag} style={{ fontSize: "0.75rem", background: "#f1f5f9", padding: "2px 8px", borderRadius: 4 }}>
{tag}
</span>
))}
<span style={{ fontSize: "0.75rem", color: "#64748b", marginLeft: "auto" }}>
{readingTime} min read
</span>
</div>
<h3 style={{ margin: "0 0 8px 0", fontSize: "1.1rem" }}>{title}</h3>
<p style={{ margin: "0 0 16px 0", fontSize: "0.875rem", color: "#475569" }}>{description}</p>
<div style={{ display: "flex", gap: 8 }}>
{actions.map((act) => (
<button
key={act.id}
onClick={() => onAction(act)}
style={{
padding: "6px 12px",
borderRadius: 4,
cursor: "pointer",
fontSize: "0.875rem",
background: act.variant === "primary" ? "#0f172a" : "#f8fafc",
color: act.variant === "primary" ? "#ffffff" : "#0f172a",
border: "1px solid #cbd5e1",
}}
>
{act.label}
</button>
))}
</div>
</div>
);
}
// Registry map
export const COMPONENT_REGISTRY: Record<string, React.ComponentType<any>> = {
BlogDiscoveryCard,
};The Dynamic Host Renderer
"use client";
import React from "react";
import { McpUiPayloadSchema, McpUiPayload, McpUiAction } from "./types";
import { COMPONENT_REGISTRY } from "./registry";
interface McpToolResource {
uri: string;
mimeType: string;
text: string;
}
interface McpToolResult {
content: Array<
| { type: "text"; text: string }
| { type: "resource"; resource: McpToolResource }
>;
}
interface McpUiHostProps {
toolResult: McpToolResult;
onDispatchTool: (toolName: string, params: Record<string, unknown>) => Promise<void>;
}
export function McpUiHost({ toolResult, onDispatchTool }: McpUiHostProps) {
// Extract UI resources
const uiResource = toolResult.content.find(
(c) => c.type === "resource" && c.resource.mimeType === "application/vnd.mcp.ui+json"
);
if (!uiResource || uiResource.type !== "resource") {
return null;
}
let parsedPayload: McpUiPayload;
try {
const rawJson = JSON.parse(uiResource.resource.text);
parsedPayload = McpUiPayloadSchema.parse(rawJson);
} catch {
return (
<div style={{ color: "#dc2626", fontSize: "0.875rem" }}>
Failed to validate MCP UI component payload.
</div>
);
}
const TargetComponent = COMPONENT_REGISTRY[parsedPayload.component];
if (!TargetComponent) {
return (
<div style={{ color: "#d97706", fontSize: "0.875rem" }}>
Unknown component '{parsedPayload.component}' requested by MCP server.
</div>
);
}
const handleAction = async (action: McpUiAction) => {
await onDispatchTool(action.tool, action.parameters);
};
return (
<div className="mcp-ui-host-container" style={{ margin: "16px 0" }}>
<TargetComponent
{...parsedPayload.props}
actions={parsedPayload.actions}
onAction={handleAction}
/>
</div>
);
}Security Boundaries & Sandboxing Untrusted Servers
बाहरी या रिमोट MCP सर्वर्स को UI परिभाषाएं लौटाने की अनुमति देना रक्षात्मक रूप से डिज़ाइन न किए जाने पर सुरक्षा जोखिम पैदा कर सकता है। हम सुरक्षा की तीन परतें लागू करते हैं:
┌──────────────────────────────────────────────────────────────────────────┐
│ Defense-in-Depth │
│ │
│ 1. Strict Schema Whitelisting (No raw HTML / eval execution) │
│ 2. Shadow DOM CSS Isolation (Prevents CSS token override / injection) │
│ 3. Sandboxed Iframe Boundary for 3rd-Party Plugins │
│ (sandbox="allow-scripts", postMessage only) │
└──────────────────────────────────────────────────────────────────────────┘1. Schema-Driven Whitelisting (Zero Eval)
कभी भी किसी MCP सर्वर को असंरचित JSX स्ट्रिंग्स, जावास्क्रिप्ट फ़ंक्शंस या प्रत्यक्ष HTML मार्कअप लौटाने की अनुमति न दें। सभी UI आउटपुट विशुद्ध रूप से Zod द्वारा सत्यापित घोषणात्मक JSON स्कीमा होने चाहिए। क्लाइंट होस्ट ज्ञात पहचानकर्ताओं (BlogDiscoveryCard, MetricGrid) को केवल आंतरिक रूप से सत्यापित React घटकों से जोड़ता है।
2. Styling Containment via Shadow DOM
अवांछित CSS नियमों (जैसे क्लिकजैकिंग के लिए अदृश्य ओवरले) को रोकने के लिए, क्लाइंट रेंडरर्स को Shadow Root में लपेटें:
import React, { useRef, useEffect } from "react";
import ReactDOM from "react-dom/client";
export function ShadowDomBoundary({ children }: { children: React.ReactNode }) {
const mountRef = useRef<HTMLDivElement>(null);
const rootRef = useRef<ReactDOM.Root | null>(null);
useEffect(() => {
if (!mountRef.current) return;
const shadow = mountRef.current.shadowRoot || mountRef.current.attachShadow({ mode: "open" });
if (!rootRef.current) {
rootRef.current = ReactDOM.createRoot(shadow);
}
rootRef.current.render(children);
return () => {
// Cleanup on unmount
};
}, [children]);
return <div ref={mountRef} />;
}3. Third-Party Plugin Isolation via Sandboxed Iframes
कस्टम लेआउट वाले तृतीय-पक्ष प्लगइन्स का समर्थन करते समय, संपूर्ण रेंडरिंग संदर्भ को <iframe> में अलग करें:
<iframe
sandbox="allow-scripts"
srcdoc="<!DOCTYPE html><html><body><div id='root'></div></body></html>"
title="MCP UI Isolated Container"
/>allow-same-origin को छोड़कर, फ्रेम किया गया घटक एक अपारदर्शी मूल में चलता है, जो होस्ट एप्लिकेशन के कुकीज़, टोकन, स्थानीय संग्रहण या मुख्य DOM तक पहुंच को रोकता है।
Headless and Terminal Fallbacks
MCP सर्वर के लिए प्रोटोकॉल लचीलापन आवश्यक है: जब कमांड-लाइन टूल, स्वचालित CI पाइपलाइन या बिना GUI वाले IDE से कॉल किया जाए, तो सर्वर को पूरी तरह से काम करना चाहिए।
दोनों परिवेशों का समर्थन करने के लिए, प्रत्येक टूल दोहरी प्रतिक्रिया उत्पन्न करता है:
import { McpUiPayload } from "./types";
interface KnowledgeArticle {
title: string;
slug: string;
description: string;
readingTime: number;
tags: string[];
}
export function buildDualMcpResponse(article: KnowledgeArticle, uiPayload: McpUiPayload) {
return {
content: [
// 1. Terminal / Markdown Fallback (Read by CLI agents)
{
type: "text",
text: [
`### ${article.title}`,
`*${article.description}*`,
`- Reading Time: ${article.readingTime} min`,
`- Tags: ${article.tags.join(", ")}`,
`- URL: https://damandeep.dev/en/journal/${article.slug}`,
].join("\n"),
},
// 2. Rich UI Resource (Rendered by GUI hosts)
{
type: "resource",
resource: {
uri: `ui://knowledge/${article.slug}`,
mimeType: "application/vnd.mcp.ui+json",
text: JSON.stringify(uiPayload),
},
},
],
};
}यदि कोई क्लाइंट application/vnd.mcp.ui+json का समर्थन नहीं करता है, तो वह केवल text फ़ील्ड को पढ़ता है और उपयोगकर्ता को बिना किसी त्रुटि के स्वच्छ मार्कडाउन आउटपुट प्रदान करता है।
आर्किटेक्चरल समझौते: कब MCP UI का उपयोग करें (और कब नहीं)
किसी भी सिस्टम डिज़ाइन पैटर्न की तरह Server-Driven UI में भी वास्तविक इंजीनियरिंग समझौते शामिल हैं:
| इंजीनियरिंग आयाम | केवल-टेक्स्ट MCP टूल्स | MCP UI (सर्वर-संचालित UI) |
|---|---|---|
| इंटरैक्शन विलंबता | उच्च (प्रत्येक क्लिक या फ़िल्टर पर पूरे LLM चक्र की आवश्यकता) | त्वरित (< 50ms इवेंट सीधे टूल पर भेजा जाता है) |
| क्लाइंट जटिलता | कम (साधारण मार्कडाउन पार्सर) | मध्यम (रजिस्ट्री रखरखाव, स्कीमा सत्यापन और एक्शन रूटिंग की आवश्यकता) |
| त्रुटि दायरा | न्यूनतम (टेक्स्ट जनरेशन त्रुटियाँ) | अज्ञात घटकों के लिए स्कीमा वर्ज़निंग और फ़ॉलबैक कार्ड की आवश्यकता |
| सुरक्षा दायरा | मानक प्रॉम्प्ट इंजेक्शन जोखिम | सख्त स्कीमा सत्यापन, CSS नियंत्रण और iframe आइसोलेशन की आवश्यकता |
व्यावहारिक निष्कर्ष
यदि आपके एजेंट का वर्कफ़्लो पूरी तरह से टेक्स्ट-आधारित है या बैकग्राउंड में अपने आप चलता है, तो केवल-टेक्स्ट टूल्स पर्याप्त हैं। लेकिन यदि आप ऐसे सॉफ़्टवेयर बना रहे हैं जहाँ इंसान फ़ैसले लेते हैं (जैसे पुल रिक्वेस्ट स्वीकार करना, इंसीडेंट्स की जाँच करना या डेटा फ़िल्टर करना), तो MCP UI एक थकाऊ टेक्स्ट वार्तालाप को आधुनिक, इंटरैक्टिव एप्लिकेशन अनुभव में बदल देता है।