@@ -19,30 +19,281 @@ import (
1919const GetMeUIResourceURI = "ui://github-mcp-server/get-me"
2020
2121// GetMeUIHTML is the HTML content for the get_me tool's MCP App UI.
22- // This is a simple "Hello World" demo with bold red text.
22+ // This UI dynamically displays user information from the tool result.
23+ //
24+ // How MCP Apps work:
25+ // 1. Server registers this HTML as a resource at ui://github-mcp-server/get-me
26+ // 2. Server links the get_me tool to this resource via _meta.ui.resourceUri
27+ // 3. When host calls get_me, it sees the resourceUri and fetches this HTML
28+ // 4. Host renders HTML in a sandboxed iframe and communicates via postMessage
29+ // 5. After ui/initialize, host sends ui/notifications/tool-result with the data
30+ // 6. This UI parses the tool result and renders the user profile dynamically
2331const GetMeUIHTML = `<!DOCTYPE html>
2432<html lang="en">
2533<head>
2634 <meta charset="UTF-8">
2735 <meta name="viewport" content="width=device-width, initial-scale=1.0">
2836 <title>GitHub MCP Server - Get Me</title>
2937 <style>
38+ * { box-sizing: border-box; }
3039 body {
3140 font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
32- padding: 20px ;
41+ padding: 16px ;
3342 margin: 0;
3443 background: var(--color-background-primary, #fff);
35- color: var(--color-text-primary, #333);
44+ color: var(--color-text-primary, #24292f);
45+ font-size: var(--font-text-md-size, 14px);
46+ line-height: var(--font-text-md-line-height, 1.5);
3647 }
37- .hello-world {
38- font-weight: bold;
39- color: red;
40- font-size: 1.5em;
48+ .loading {
49+ color: var(--color-text-secondary, #656d76);
50+ font-style: italic;
51+ }
52+ .error {
53+ color: var(--color-text-danger, #cf222e);
54+ }
55+ .user-card {
56+ border: 1px solid var(--color-border-primary, #d0d7de);
57+ border-radius: var(--border-radius-lg, 6px);
58+ padding: 16px;
59+ max-width: 400px;
60+ background: var(--color-background-secondary, #f6f8fa);
61+ }
62+ .user-header {
63+ display: flex;
64+ align-items: center;
65+ gap: 12px;
66+ margin-bottom: 12px;
67+ padding-bottom: 12px;
68+ border-bottom: 1px solid var(--color-border-primary, #d0d7de);
69+ }
70+ .avatar {
71+ width: 48px;
72+ height: 48px;
73+ border-radius: 50%;
74+ border: 1px solid var(--color-border-primary, #d0d7de);
75+ }
76+ .user-names {
77+ flex: 1;
78+ }
79+ .display-name {
80+ font-weight: 600;
81+ font-size: var(--font-heading-xs-size, 16px);
82+ color: var(--color-text-primary, #24292f);
83+ margin: 0;
84+ }
85+ .username {
86+ color: var(--color-text-secondary, #656d76);
87+ margin: 0;
88+ }
89+ .bio {
90+ font-style: italic;
91+ color: var(--color-text-secondary, #656d76);
92+ margin: 0 0 12px 0;
93+ padding: 8px;
94+ background: var(--color-background-primary, #fff);
95+ border-radius: var(--border-radius-sm, 4px);
96+ }
97+ .info-grid {
98+ display: grid;
99+ grid-template-columns: auto 1fr;
100+ gap: 4px 12px;
101+ }
102+ .info-label {
103+ color: var(--color-text-secondary, #656d76);
104+ font-weight: 500;
105+ }
106+ .info-value {
107+ color: var(--color-text-primary, #24292f);
108+ }
109+ .info-value a {
110+ color: var(--color-text-info, #0969da);
111+ text-decoration: none;
112+ }
113+ .info-value a:hover {
114+ text-decoration: underline;
115+ }
116+ .stats {
117+ display: flex;
118+ gap: 16px;
119+ margin-top: 12px;
120+ padding-top: 12px;
121+ border-top: 1px solid var(--color-border-primary, #d0d7de);
122+ }
123+ .stat {
124+ text-align: center;
125+ }
126+ .stat-value {
127+ font-weight: 600;
128+ font-size: var(--font-heading-xs-size, 16px);
129+ color: var(--color-text-primary, #24292f);
130+ }
131+ .stat-label {
132+ font-size: var(--font-text-sm-size, 12px);
133+ color: var(--color-text-secondary, #656d76);
41134 }
42135 </style>
43136</head>
44137<body>
45- <p class="hello-world">Hello World</p>
138+ <div id="content">
139+ <p class="loading">Loading user data...</p>
140+ </div>
141+ <script>
142+ // ============================================================
143+ // MCP Apps Protocol Implementation
144+ // ============================================================
145+ // MCP Apps communicate with the host via postMessage using JSON-RPC.
146+ // The host (e.g., VS Code) renders this HTML in a sandboxed iframe.
147+ // After initialization, the host pushes the tool result to us.
148+
149+ const pendingRequests = new Map();
150+ let requestId = 0;
151+
152+ // Send a JSON-RPC request to the host and wait for response
153+ function sendRequest(method, params) {
154+ const id = ++requestId;
155+ return new Promise((resolve) => {
156+ pendingRequests.set(id, resolve);
157+ window.parent.postMessage({ jsonrpc: '2.0', id, method, params }, '*');
158+ });
159+ }
160+
161+ // Handle all messages from the host
162+ function handleMessage(event) {
163+ const message = event.data;
164+ if (!message || typeof message !== 'object') return;
165+
166+ // Handle responses to our requests (e.g., ui/initialize response)
167+ if (message.id !== undefined && pendingRequests.has(message.id)) {
168+ const resolve = pendingRequests.get(message.id);
169+ pendingRequests.delete(message.id);
170+ resolve(message.result);
171+ return;
172+ }
173+
174+ // Handle notifications pushed by the host
175+ // The host sends ui/notifications/tool-result after the tool completes
176+ if (message.method === 'ui/notifications/tool-result') {
177+ handleToolResult(message.params);
178+ }
179+ }
180+
181+ // Process the tool result and render the UI
182+ function handleToolResult(result) {
183+ if (!result || !result.content) {
184+ showError('No content in tool result');
185+ return;
186+ }
187+
188+ // The tool result contains content blocks; find the text one
189+ const textContent = result.content.find(c => c.type === 'text');
190+ if (!textContent || !textContent.text) {
191+ showError('No text content in tool result');
192+ return;
193+ }
194+
195+ try {
196+ // Parse the JSON user data from the tool's text output
197+ const userData = JSON.parse(textContent.text);
198+ renderUserCard(userData);
199+ } catch (e) {
200+ showError('Error parsing user data: ' + e.message);
201+ }
202+ }
203+
204+ function showError(msg) {
205+ document.getElementById('content').innerHTML =
206+ '<p class="error">' + escapeHtml(msg) + '</p>';
207+ notifySize();
208+ }
209+
210+ function renderUserCard(user) {
211+ const d = user.details || {};
212+ const html = ` + "`" + `
213+ <div class="user-card">
214+ <div class="user-header">
215+ ${user.avatar_url ? ` + "`" + `<img class="avatar" src="${escapeHtml(user.avatar_url)}" alt="Avatar">` + "`" + ` : ''}
216+ <div class="user-names">
217+ <p class="display-name">${escapeHtml(d.name || user.login || 'Unknown')}</p>
218+ <p class="username">@${escapeHtml(user.login || 'unknown')}</p>
219+ </div>
220+ </div>
221+ <div class="info-grid">
222+ ${d.company ? ` + "`" + `
223+ <span class="info-label">🏢 Company</span>
224+ <span class="info-value">${escapeHtml(d.company)}</span>
225+ ` + "`" + ` : ''}
226+ ${d.location ? ` + "`" + `
227+ <span class="info-label">📍 Location</span>
228+ <span class="info-value">${escapeHtml(d.location)}</span>
229+ ` + "`" + ` : ''}
230+ ${d.blog ? ` + "`" + `
231+ <span class="info-label">🔗 Website</span>
232+ <span class="info-value"><a href="${escapeHtml(d.blog)}" target="_blank">${escapeHtml(d.blog)}</a></span>
233+ ` + "`" + ` : ''}
234+ ${d.twitter_username ? ` + "`" + `
235+ <span class="info-label">🐦 Twitter</span>
236+ <span class="info-value"><a href="https://twitter.com/${escapeHtml(d.twitter_username)}" target="_blank">@${escapeHtml(d.twitter_username)}</a></span>
237+ ` + "`" + ` : ''}
238+ ${d.email ? ` + "`" + `
239+ <span class="info-label">✉️ Email</span>
240+ <span class="info-value"><a href="mailto:${escapeHtml(d.email)}">${escapeHtml(d.email)}</a></span>
241+ ` + "`" + ` : ''}
242+ </div>
243+ <div class="stats">
244+ <div class="stat">
245+ <div class="stat-value">${d.public_repos ?? 0}</div>
246+ <div class="stat-label">Repos</div>
247+ </div>
248+ <div class="stat">
249+ <div class="stat-value">${d.followers ?? 0}</div>
250+ <div class="stat-label">Followers</div>
251+ </div>
252+ <div class="stat">
253+ <div class="stat-value">${d.following ?? 0}</div>
254+ <div class="stat-label">Following</div>
255+ </div>
256+ <div class="stat">
257+ <div class="stat-value">${d.public_gists ?? 0}</div>
258+ <div class="stat-label">Gists</div>
259+ </div>
260+ </div>
261+ </div>
262+ ` + "`" + `;
263+
264+ document.getElementById('content').innerHTML = html;
265+ notifySize();
266+ }
267+
268+ // Tell the host our rendered size so it can adjust the iframe
269+ function notifySize() {
270+ window.parent.postMessage({
271+ jsonrpc: '2.0',
272+ method: 'ui/notifications/size-changed',
273+ params: { height: document.body.scrollHeight + 20 }
274+ }, '*');
275+ }
276+
277+ function escapeHtml(text) {
278+ if (text == null) return '';
279+ const div = document.createElement('div');
280+ div.textContent = String(text);
281+ return div.innerHTML;
282+ }
283+
284+ // Listen for messages from the host
285+ window.addEventListener('message', handleMessage);
286+
287+ // Initialize the MCP App connection
288+ // After this completes, the host will send us:
289+ // 1. ui/notifications/tool-input (the arguments passed to the tool)
290+ // 2. ui/notifications/tool-result (the tool's output - what we render)
291+ sendRequest('ui/initialize', {
292+ appInfo: { name: 'github-mcp-server-get-me', version: '1.0.0' },
293+ appCapabilities: {},
294+ protocolVersion: '2025-11-21'
295+ });
296+ </script>
46297</body>
47298</html>`
48299
0 commit comments