Skip to content

Commit 03b2be3

Browse files
committed
feat: add view tag tool and UI for tag details
1 parent de850f8 commit 03b2be3

File tree

2 files changed

+93
-66
lines changed

2 files changed

+93
-66
lines changed

exercises/99.final/99.solution/src/tools.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js'
1818
import { z } from 'zod'
1919
import { type EpicMeMCP } from './index.ts'
2020
import { suggestTagsSampling } from './sampling.ts'
21+
import { getTagRemoteDomUIScript, getTagViewUI } from './ui.ts'
2122

2223
export async function initializeTools(agent: EpicMeMCP) {
2324
agent.server.registerTool(
@@ -63,7 +64,7 @@ export async function initializeTools(agent: EpicMeMCP) {
6364
'view_journal',
6465
{
6566
title: 'View Journal',
66-
description: 'View the journal',
67+
description: 'View the journal visually',
6768
annotations: {
6869
readOnlyHint: true,
6970
openWorldHint: false,
@@ -250,6 +251,33 @@ export async function initializeTools(agent: EpicMeMCP) {
250251
},
251252
)
252253

254+
agent.server.registerTool(
255+
'view_tag',
256+
{
257+
title: 'View Tag',
258+
description: 'View a tag by ID visually',
259+
annotations: {
260+
readOnlyHint: true,
261+
openWorldHint: false,
262+
},
263+
inputSchema: tagIdSchema,
264+
},
265+
async ({ id }) => {
266+
return {
267+
content: [
268+
createUIResource({
269+
uri: `ui://view-tag/${id}`,
270+
content: {
271+
type: 'rawHtml',
272+
htmlString: await getTagViewUI(agent.db, id),
273+
},
274+
encoding: 'text',
275+
}),
276+
],
277+
}
278+
},
279+
)
280+
253281
agent.server.registerTool(
254282
'get_tag',
255283
{

exercises/99.final/99.solution/src/ui.ts

Lines changed: 64 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,68 @@ export async function getJournalViewUI(db: DBClient) {
1616
`
1717
}
1818

19-
export async function getEntryViewUI(db: DBClient, entryId: number) {
20-
const entry = await db.getEntry(entryId)
21-
if (!entry) {
19+
export async function getTagRemoteDomUIScript(db: DBClient, tagId: number) {
20+
const tag = await db.getTag(tagId)
21+
if (!tag) {
22+
return /* js */ `
23+
console.error('Tag not found');
24+
const stack = document.createElement('ui-stack');
25+
stack.setAttribute('direction', 'vertical');
26+
stack.setAttribute('spacing', '20');
27+
stack.setAttribute('align', 'center');
28+
const title = document.createElement('ui-text');
29+
title.setAttribute('content', 'Tag not found');
30+
stack.appendChild(title);
31+
root.appendChild(stack);
32+
`
33+
} else {
34+
return /* js */ `
35+
console.log('Tag found:', ${tag.name});
36+
const stack = document.createElement('ui-stack');
37+
stack.setAttribute('direction', 'vertical');
38+
stack.setAttribute('spacing', '20');
39+
stack.setAttribute('align', 'center');
40+
41+
const title = document.createElement('ui-text');
42+
title.setAttribute('content', ${JSON.stringify(tag.name)});
43+
stack.appendChild(title);
44+
45+
const description = document.createElement('ui-text');
46+
description.setAttribute('content', ${JSON.stringify(tag.description)});
47+
stack.appendChild(description);
48+
49+
const deleteButton = document.createElement('ui-button');
50+
deleteButton.setAttribute('content', 'Delete Tag');
51+
deleteButton.addEventListener('press', () => {
52+
window.parent.postMessage(
53+
{
54+
type: 'tool',
55+
payload: {
56+
toolName: 'deleteTag',
57+
params: { tagId: tag.id.toString() },
58+
},
59+
},
60+
'*',
61+
)
62+
});
63+
stack.appendChild(deleteButton);
64+
65+
root.appendChild(stack);
66+
`
67+
}
68+
}
69+
70+
export async function getTagViewUI(db: DBClient, tagId: number) {
71+
const tag = await db.getTag(tagId)
72+
if (!tag) {
2273
return /* html */ `
2374
<html>
2475
${getHead()}
2576
<body>
2677
<div class="container">
2778
<div class="error-state">
2879
${createIcon('alert-circle', 'error-icon')}
29-
<h1>Entry not found</h1>
30-
${createButton('Go Back', '/', 'secondary')}
80+
<h1>Tag with id ${tagId} not found</h1>
3181
</div>
3282
</div>
3383
</html>
@@ -38,16 +88,8 @@ export async function getEntryViewUI(db: DBClient, entryId: number) {
3888
${getHead()}
3989
<body>
4090
<div class="container">
41-
<h1 class="title">${entry.title}</h1>
42-
<article class="entry-content">
43-
<div class="entry-meta">
44-
${createIcon('calendar', 'meta-icon')}
45-
<span>Created: ${new Date().toLocaleDateString()}</span>
46-
</div>
47-
<div class="content">
48-
${entry.content}
49-
</div>
50-
</article>
91+
<h1 class="title">${tag.name}</h1>
92+
<p class="description">${tag.description}</p>
5193
</div>
5294
</html>
5395
`
@@ -56,38 +98,30 @@ export async function getEntryViewUI(db: DBClient, entryId: number) {
5698
// UI Components
5799
function createButton(
58100
text: string,
59-
href?: string,
60101
variant: 'primary' | 'secondary' = 'primary',
61102
) {
62103
const baseClass = `btn btn-${variant}`
63-
if (href) {
64-
return `<a href="${href}" class="${baseClass}">${text}</a>`
65-
}
66104
return `<button class="${baseClass}">${text}</button>`
67105
}
68106

69-
function createLink(href: string, text: string, iconName?: string) {
70-
const icon = iconName ? createIcon(iconName, 'link-icon') : ''
71-
return `<a href="${href}" class="link">${icon}${text}</a>`
72-
}
73-
74107
function createIcon(name: string, className?: string) {
75108
const iconClass = className ? `icon ${className}` : 'icon'
76109
return `<span class="${iconClass}" data-icon="${name}">${getIconSvg(name)}</span>`
77110
}
78111

79-
function createEntryCard(entry: any) {
112+
function createEntryCard(entry: {
113+
id: number
114+
title: string
115+
tagCount: number
116+
}) {
80117
return /* html */ `
81118
<article class="entry-card">
82119
<div class="card-header">
83120
${createIcon('file-text', 'card-icon')}
84121
<h3 class="card-title">${entry.title}</h3>
85122
</div>
86123
<div class="card-content">
87-
<p>${entry.content.substring(0, 100)}${entry.content.length > 100 ? '...' : ''}</p>
88-
</div>
89-
<div class="card-actions">
90-
${createButton('View', `/entry/${entry.id}`, 'primary')}
124+
<p>${entry.tagCount} tags</p>
91125
</div>
92126
</article>
93127
`
@@ -273,38 +307,7 @@ function getStyles() {
273307
margin-bottom: 1.5rem;
274308
color: #64748b;
275309
}
276-
277-
.card-actions {
278-
display: flex;
279-
justify-content: flex-end;
280-
}
281-
282-
/* Entry Content */
283-
.entry-content {
284-
background: white;
285-
padding: 2rem;
286-
border-radius: 0.75rem;
287-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
288-
border: 1px solid #e2e8f0;
289-
}
290-
291-
.entry-meta {
292-
display: flex;
293-
align-items: center;
294-
gap: 0.5rem;
295-
margin-bottom: 1.5rem;
296-
padding-bottom: 1rem;
297-
border-bottom: 1px solid #e2e8f0;
298-
color: #64748b;
299-
font-size: 0.875rem;
300-
}
301-
302-
.content {
303-
font-size: 1.125rem;
304-
line-height: 1.7;
305-
color: #334155;
306-
}
307-
310+
308311
/* Error State */
309312
.error-state {
310313
text-align: center;
@@ -321,10 +324,6 @@ function getStyles() {
321324
.entries-grid {
322325
grid-template-columns: 1fr;
323326
}
324-
325-
.entry-content {
326-
padding: 1.5rem;
327-
}
328327
}
329328
`
330329
}

0 commit comments

Comments
 (0)