@ssemble/mcp-server 1.0.6 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +8 -1
  2. package/server.json +3 -3
  3. package/src/apps/dist/game-videos-app.html +163 -0
  4. package/src/apps/dist/meme-hooks-app.html +165 -0
  5. package/src/apps/dist/music-app.html +174 -0
  6. package/src/apps/dist/shorts-app.html +218 -0
  7. package/src/apps/dist/templates-app.html +157 -0
  8. package/src/apps/game-videos-app.html +92 -0
  9. package/src/apps/game-videos-app.js +102 -0
  10. package/src/apps/meme-hooks-app.html +95 -0
  11. package/src/apps/meme-hooks-app.js +94 -0
  12. package/src/apps/music-app.html +96 -0
  13. package/src/apps/music-app.js +151 -0
  14. package/src/apps/shorts-app.html +135 -0
  15. package/src/apps/shorts-app.js +102 -0
  16. package/src/apps/templates-app.html +87 -0
  17. package/src/apps/templates-app.js +74 -0
  18. package/src/assets/thumbnails/ali.jpg +0 -0
  19. package/src/assets/thumbnails/beast.jpg +0 -0
  20. package/src/assets/thumbnails/box.jpg +0 -0
  21. package/src/assets/thumbnails/hormozi1.jpg +0 -0
  22. package/src/assets/thumbnails/hormozi2.jpg +0 -0
  23. package/src/assets/thumbnails/iman.jpg +0 -0
  24. package/src/assets/thumbnails/iman2.jpg +0 -0
  25. package/src/assets/thumbnails/karaoke.jpg +0 -0
  26. package/src/assets/thumbnails/simple.jpg +0 -0
  27. package/src/index.js +2 -0
  28. package/src/resources/app-resources.js +149 -0
  29. package/src/tools/create-short.js +10 -0
  30. package/src/tools/get-shorts.js +57 -6
  31. package/src/tools/get-status.js +20 -3
  32. package/src/tools/list-game-videos.js +34 -4
  33. package/src/tools/list-meme-hooks.js +34 -4
  34. package/src/tools/list-music.js +34 -4
  35. package/src/tools/list-templates.js +64 -5
  36. package/src/utils/format.js +41 -9
  37. package/src/utils/session-store.js +30 -0
@@ -0,0 +1,87 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Ssemble Templates</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
11
+ background: var(--color-background-primary, #1a1a2e);
12
+ color: var(--color-text-primary, #e0e0e0);
13
+ padding: 16px;
14
+ }
15
+ h2 { font-size: 18px; margin-bottom: 16px; color: var(--color-text-primary, #fff); }
16
+ .grid {
17
+ display: grid;
18
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
19
+ gap: 12px;
20
+ }
21
+ .card {
22
+ background: var(--color-background-secondary, #16213e);
23
+ border-radius: var(--border-radius-md, 8px);
24
+ overflow: hidden;
25
+ cursor: pointer;
26
+ border: 2px solid transparent;
27
+ transition: border-color 0.2s, transform 0.15s;
28
+ }
29
+ .card:hover { border-color: var(--color-border-primary, #4a6cf7); transform: translateY(-2px); }
30
+ .card.selected { border-color: var(--color-border-primary, #4a6cf7); box-shadow: 0 0 12px rgba(74,108,247,0.3); }
31
+ .card.default { border-color: #f7c94a; }
32
+ .card img {
33
+ width: 100%;
34
+ aspect-ratio: 9/16;
35
+ object-fit: cover;
36
+ display: block;
37
+ background: var(--color-background-tertiary, #0f3460);
38
+ }
39
+ .card .placeholder {
40
+ width: 100%;
41
+ aspect-ratio: 9/16;
42
+ background: var(--color-background-tertiary, #0f3460);
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ color: var(--color-text-secondary, #555);
47
+ font-size: 12px;
48
+ }
49
+ .card-label {
50
+ padding: 8px;
51
+ font-size: 13px;
52
+ text-align: center;
53
+ white-space: nowrap;
54
+ overflow: hidden;
55
+ text-overflow: ellipsis;
56
+ }
57
+ .badge {
58
+ display: inline-block;
59
+ font-size: 10px;
60
+ background: #f7c94a;
61
+ color: #1a1a2e;
62
+ padding: 2px 6px;
63
+ border-radius: 4px;
64
+ margin-left: 4px;
65
+ font-weight: 600;
66
+ }
67
+ .loading { text-align: center; padding: 40px; color: var(--color-text-secondary, #888); }
68
+ .selected-info {
69
+ margin-top: 16px;
70
+ padding: 12px;
71
+ background: var(--color-background-secondary, #16213e);
72
+ border-radius: var(--border-radius-md, 8px);
73
+ font-size: 13px;
74
+ }
75
+ .selected-info code {
76
+ background: var(--color-background-tertiary, #0f3460);
77
+ padding: 2px 6px;
78
+ border-radius: 4px;
79
+ font-family: var(--font-mono, monospace);
80
+ }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div id="app"><div class="loading">Loading templates...</div></div>
85
+ <script type="module" src="./templates-app.js"></script>
86
+ </body>
87
+ </html>
@@ -0,0 +1,74 @@
1
+ import { App } from '@modelcontextprotocol/ext-apps';
2
+
3
+ const appEl = document.getElementById('app');
4
+ const app = new App({ name: 'Ssemble Templates', version: '1.0.0' });
5
+
6
+ let selectedTemplateId = null;
7
+ let cachedData = null;
8
+
9
+ function render(data) {
10
+ if (!data || !data.templates) {
11
+ appEl.innerHTML = '<div class="loading">No template data received.</div>';
12
+ return;
13
+ }
14
+
15
+ cachedData = data;
16
+ const { templates, defaultTemplateId } = data;
17
+
18
+ let html = `<h2>Caption Templates (${templates.length})</h2>`;
19
+ html += '<div class="grid">';
20
+
21
+ templates.forEach((t) => {
22
+ const isDefault = t.templateId === defaultTemplateId;
23
+ const isSelected = t.templateId === selectedTemplateId;
24
+ const classes = ['card'];
25
+ if (isDefault && !isSelected) classes.push('default');
26
+ if (isSelected) classes.push('selected');
27
+
28
+ const imgSrc = t.preview || t.gif || '';
29
+ const imgTag = imgSrc
30
+ ? `<img src="${imgSrc}" alt="${t.displayName}" loading="lazy" />`
31
+ : `<div class="placeholder">No preview</div>`;
32
+
33
+ html += `<div class="${classes.join(' ')}" data-id="${t.templateId}" data-name="${t.name}">
34
+ ${imgTag}
35
+ <div class="card-label">
36
+ ${t.displayName}${isDefault ? '<span class="badge">default</span>' : ''}
37
+ </div>
38
+ </div>`;
39
+ });
40
+
41
+ html += '</div>';
42
+
43
+ if (selectedTemplateId) {
44
+ const sel = templates.find((t) => t.templateId === selectedTemplateId);
45
+ if (sel) {
46
+ html += `<div class="selected-info">
47
+ Selected: <strong>${sel.displayName}</strong> — templateId: <code>${sel.templateId}</code>
48
+ </div>`;
49
+ }
50
+ }
51
+
52
+ appEl.innerHTML = html;
53
+
54
+ appEl.querySelectorAll('.card').forEach((card) => {
55
+ card.addEventListener('click', () => {
56
+ selectedTemplateId = card.dataset.id;
57
+ render(cachedData);
58
+ app.updateModelContext({
59
+ text: `User selected template: ${card.dataset.name} (templateId: ${card.dataset.id}). Use this templateId when creating shorts.`,
60
+ });
61
+ });
62
+ });
63
+ }
64
+
65
+ app.ontoolresult = (result) => {
66
+ const data = result.structuredContent || null;
67
+ if (data) {
68
+ render(data);
69
+ } else {
70
+ appEl.innerHTML = '<div class="loading">No structured data available.</div>';
71
+ }
72
+ };
73
+
74
+ app.connect();
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/index.js CHANGED
@@ -19,6 +19,7 @@ import { registerListMemeHooks } from './tools/list-meme-hooks.js';
19
19
 
20
20
  // Resources & Prompts
21
21
  import { registerResources } from './resources/static-data.js';
22
+ import { registerAppResources } from './resources/app-resources.js';
22
23
  import { registerPrompts } from './prompts/workflows.js';
23
24
 
24
25
  export function createSsembleMcpServer(apiKey, baseUrl) {
@@ -44,6 +45,7 @@ export function createSsembleMcpServer(apiKey, baseUrl) {
44
45
 
45
46
  // Register resources
46
47
  registerResources(server);
48
+ registerAppResources(server);
47
49
 
48
50
  // Register prompts
49
51
  registerPrompts(server);
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Registers MCP App UI resources for interactive HTML views.
3
+ */
4
+
5
+ import { registerAppResource } from '@modelcontextprotocol/ext-apps/server';
6
+ import { readFileSync } from 'fs';
7
+ import { dirname, join } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const DIST_DIR = join(__dirname, '..', 'apps', 'dist');
12
+
13
+ function loadHtml(filename) {
14
+ return readFileSync(join(DIST_DIR, filename), 'utf-8');
15
+ }
16
+
17
+ const CDN_DOMAINS = ['https://cf.ssemble.com', 'https://*.ssemble.com'];
18
+
19
+ export function registerAppResources(server) {
20
+ registerAppResource(
21
+ server,
22
+ 'Templates Browser',
23
+ 'ui://ssemble/templates-app.html',
24
+ {
25
+ description: 'Interactive grid of caption style templates with GIF previews',
26
+ _meta: {
27
+ ui: {
28
+ csp: {
29
+ resourceDomains: CDN_DOMAINS,
30
+ connectDomains: CDN_DOMAINS,
31
+ },
32
+ },
33
+ },
34
+ },
35
+ async () => ({
36
+ contents: [
37
+ {
38
+ uri: 'ui://ssemble/templates-app.html',
39
+ mimeType: 'text/html;profile=mcp-app',
40
+ text: loadHtml('templates-app.html'),
41
+ },
42
+ ],
43
+ })
44
+ );
45
+
46
+ registerAppResource(
47
+ server,
48
+ 'Music Player',
49
+ 'ui://ssemble/music-app.html',
50
+ {
51
+ description: 'Interactive music player with audio preview and track selection',
52
+ _meta: {
53
+ ui: {
54
+ csp: {
55
+ resourceDomains: CDN_DOMAINS,
56
+ connectDomains: CDN_DOMAINS,
57
+ },
58
+ },
59
+ },
60
+ },
61
+ async () => ({
62
+ contents: [
63
+ {
64
+ uri: 'ui://ssemble/music-app.html',
65
+ mimeType: 'text/html;profile=mcp-app',
66
+ text: loadHtml('music-app.html'),
67
+ },
68
+ ],
69
+ })
70
+ );
71
+
72
+ registerAppResource(
73
+ server,
74
+ 'Game Videos Gallery',
75
+ 'ui://ssemble/game-videos-app.html',
76
+ {
77
+ description: 'Interactive gallery of gameplay overlay videos with preview images',
78
+ _meta: {
79
+ ui: {
80
+ csp: {
81
+ resourceDomains: CDN_DOMAINS,
82
+ connectDomains: CDN_DOMAINS,
83
+ },
84
+ },
85
+ },
86
+ },
87
+ async () => ({
88
+ contents: [
89
+ {
90
+ uri: 'ui://ssemble/game-videos-app.html',
91
+ mimeType: 'text/html;profile=mcp-app',
92
+ text: loadHtml('game-videos-app.html'),
93
+ },
94
+ ],
95
+ })
96
+ );
97
+
98
+ registerAppResource(
99
+ server,
100
+ 'Meme Hooks Gallery',
101
+ 'ui://ssemble/meme-hooks-app.html',
102
+ {
103
+ description: 'Interactive gallery of meme hook clips with preview images',
104
+ _meta: {
105
+ ui: {
106
+ csp: {
107
+ resourceDomains: CDN_DOMAINS,
108
+ connectDomains: CDN_DOMAINS,
109
+ },
110
+ },
111
+ },
112
+ },
113
+ async () => ({
114
+ contents: [
115
+ {
116
+ uri: 'ui://ssemble/meme-hooks-app.html',
117
+ mimeType: 'text/html;profile=mcp-app',
118
+ text: loadHtml('meme-hooks-app.html'),
119
+ },
120
+ ],
121
+ })
122
+ );
123
+
124
+ registerAppResource(
125
+ server,
126
+ 'Shorts Viewer',
127
+ 'ui://ssemble/shorts-app.html',
128
+ {
129
+ description: 'Interactive viewer for generated short clips with video player and viral scores',
130
+ _meta: {
131
+ ui: {
132
+ csp: {
133
+ resourceDomains: CDN_DOMAINS,
134
+ connectDomains: CDN_DOMAINS,
135
+ },
136
+ },
137
+ },
138
+ },
139
+ async () => ({
140
+ contents: [
141
+ {
142
+ uri: 'ui://ssemble/shorts-app.html',
143
+ mimeType: 'text/html;profile=mcp-app',
144
+ text: loadHtml('shorts-app.html'),
145
+ },
146
+ ],
147
+ })
148
+ );
149
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { formatCreateShortResponse, formatError } from '../utils/format.js';
3
+ import { saveRequest } from '../utils/session-store.js';
3
4
 
4
5
  const SUPPORTED_LANGUAGES = [
5
6
  'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh',
@@ -55,6 +56,15 @@ export function registerCreateShort(server, client) {
55
56
 
56
57
  try {
57
58
  const result = await client.createShort(params);
59
+ const requestId = result.data?.requestId;
60
+ if (requestId) {
61
+ saveRequest(requestId, {
62
+ url: params.url || params.fileUrl || '',
63
+ title: null,
64
+ status: 'queued',
65
+ createdAt: new Date().toISOString(),
66
+ });
67
+ }
58
68
  return { content: [{ type: 'text', text: formatCreateShortResponse(result) }] };
59
69
  } catch (error) {
60
70
  return { content: [{ type: 'text', text: formatError(error) }], isError: true };
@@ -1,19 +1,70 @@
1
1
  import { z } from 'zod';
2
- import { formatShortsResponse, formatError } from '../utils/format.js';
2
+ import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
3
+ import { formatShortsResponse, formatRecentRequests, formatError } from '../utils/format.js';
4
+ import { getRecentRequests, hasRequests, updateRequest } from '../utils/session-store.js';
3
5
 
4
6
  const schema = {
5
- requestId: z.string().regex(/^[0-9a-fA-F]{24}$/).describe('The request ID from create_short (24-char hex)'),
7
+ requestId: z.string().regex(/^[0-9a-fA-F]{24}$/).describe('The request ID from create_short (24-char hex). Omit to see your recent requests.').optional(),
6
8
  };
7
9
 
8
10
  export function registerGetShorts(server, client) {
9
- server.tool(
11
+ registerAppTool(
12
+ server,
10
13
  'get_shorts',
11
- 'Retrieve all generated short clips for a completed request. Returns video URLs, AI-generated titles, descriptions, viral scores, and durations sorted by viral score.',
12
- schema,
14
+ {
15
+ title: 'Get Shorts',
16
+ description:
17
+ 'Retrieve all generated short clips for a completed request. Provide a request ID, or omit it to see your recent requests and pick one.',
18
+ inputSchema: schema,
19
+ _meta: {
20
+ ui: { resourceUri: 'ui://ssemble/shorts-app.html' },
21
+ },
22
+ },
13
23
  async ({ requestId }) => {
24
+ if (!requestId) {
25
+ // No ID given — show recent requests so user can pick
26
+ if (hasRequests()) {
27
+ const recent = getRecentRequests(5);
28
+ return { content: [{ type: 'text', text: formatRecentRequests(recent, 'get_shorts') }] };
29
+ }
30
+ try {
31
+ const result = await client.listRequests({ limit: 5, sortOrder: 'desc' });
32
+ return { content: [{ type: 'text', text: formatRecentRequests(result.data?.requests || [], 'get_shorts', true) }] };
33
+ } catch (error) {
34
+ return { content: [{ type: 'text', text: formatError(error) }], isError: true };
35
+ }
36
+ }
37
+
14
38
  try {
15
39
  const result = await client.getShorts(requestId);
16
- return { content: [{ type: 'text', text: formatShortsResponse(result) }] };
40
+ // Save the video title back to session store for future lookups
41
+ if (result.data?.title) {
42
+ updateRequest(requestId, { title: result.data.title, status: result.data.status });
43
+ }
44
+ const textFallback = formatShortsResponse(result);
45
+ const d = result.data;
46
+
47
+ const structuredContent = {
48
+ title: d.title || '',
49
+ url: d.url || '',
50
+ start: d.start,
51
+ end: d.end,
52
+ language: d.language || '',
53
+ template: d.template || '',
54
+ shorts: (d.shorts || []).map((s) => ({
55
+ title: s.title || '',
56
+ description: s.description || '',
57
+ duration: s.duration || 0,
58
+ viral_score: s.viral_score != null ? s.viral_score : 0,
59
+ video_url: s.video_url || '',
60
+ error: !!s.error,
61
+ })),
62
+ };
63
+
64
+ return {
65
+ content: [{ type: 'text', text: textFallback }],
66
+ structuredContent,
67
+ };
17
68
  } catch (error) {
18
69
  return { content: [{ type: 'text', text: formatError(error) }], isError: true };
19
70
  }
@@ -1,18 +1,35 @@
1
1
  import { z } from 'zod';
2
- import { formatStatusResponse, formatError } from '../utils/format.js';
2
+ import { formatStatusResponse, formatRecentRequests, formatError } from '../utils/format.js';
3
+ import { getRecentRequests, hasRequests, updateRequest } from '../utils/session-store.js';
3
4
 
4
5
  const schema = {
5
- requestId: z.string().regex(/^[0-9a-fA-F]{24}$/).describe('The request ID from create_short (24-char hex)'),
6
+ requestId: z.string().regex(/^[0-9a-fA-F]{24}$/).describe('The request ID from create_short (24-char hex). Omit to see your recent requests.').optional(),
6
7
  };
7
8
 
8
9
  export function registerGetStatus(server, client) {
9
10
  server.tool(
10
11
  'get_status',
11
- 'Check processing status and progress of a short creation request. Returns status (queued/processing/completed/failed), progress percentage, and current step.',
12
+ 'Check processing status of a short creation request. Provide a request ID, or omit it to see your recent requests and pick one.',
12
13
  schema,
13
14
  async ({ requestId }) => {
15
+ if (!requestId) {
16
+ // No ID given — show recent requests so user can pick
17
+ if (hasRequests()) {
18
+ const recent = getRecentRequests(5);
19
+ return { content: [{ type: 'text', text: formatRecentRequests(recent, 'get_status') }] };
20
+ }
21
+ // Nothing in session — fall back to API list
22
+ try {
23
+ const result = await client.listRequests({ limit: 5, sortOrder: 'desc' });
24
+ return { content: [{ type: 'text', text: formatRecentRequests(result.data?.requests || [], 'get_status', true) }] };
25
+ } catch (error) {
26
+ return { content: [{ type: 'text', text: formatError(error) }], isError: true };
27
+ }
28
+ }
29
+
14
30
  try {
15
31
  const result = await client.getStatus(requestId);
32
+ updateRequest(requestId, { status: result.data?.status });
16
33
  return { content: [{ type: 'text', text: formatStatusResponse(result) }] };
17
34
  } catch (error) {
18
35
  return { content: [{ type: 'text', text: formatError(error) }], isError: true };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
2
3
  import { formatPaginatedAssets, formatError } from '../utils/format.js';
3
4
 
4
5
  const schema = {
@@ -7,14 +8,43 @@ const schema = {
7
8
  };
8
9
 
9
10
  export function registerListGameVideos(server, client) {
10
- server.tool(
11
+ registerAppTool(
12
+ server,
11
13
  'list_game_videos',
12
- 'List available gameplay videos for split-screen overlays (content top, game bottom). Use the exact gameVideoName when creating shorts.',
13
- schema,
14
+ {
15
+ title: 'List Game Videos',
16
+ description:
17
+ 'List available gameplay videos for split-screen overlays (content top, game bottom). Use the exact gameVideoName when creating shorts.',
18
+ inputSchema: schema,
19
+ _meta: {
20
+ ui: { resourceUri: 'ui://ssemble/game-videos-app.html' },
21
+ },
22
+ },
14
23
  async (params) => {
15
24
  try {
16
25
  const result = await client.listGameVideos(params);
17
- return { content: [{ type: 'text', text: formatPaginatedAssets(result, 'gameVideos', 'Gameplay Overlays') }] };
26
+ const textFallback = formatPaginatedAssets(result, 'gameVideos', 'Gameplay Overlays');
27
+
28
+ const items = result.data?.gameVideos || [];
29
+ const pagination = result.data?.pagination || {};
30
+ const structuredContent = {
31
+ gameVideos: items.map((g) => ({
32
+ name: g.name,
33
+ preview: g.preview || '',
34
+ video: g.video || '',
35
+ video_duration: g.video_duration || 0,
36
+ })),
37
+ pagination: {
38
+ page: pagination.page || 1,
39
+ totalPages: pagination.totalPages || 1,
40
+ totalCount: pagination.totalCount || items.length,
41
+ },
42
+ };
43
+
44
+ return {
45
+ content: [{ type: 'text', text: textFallback }],
46
+ structuredContent,
47
+ };
18
48
  } catch (error) {
19
49
  return { content: [{ type: 'text', text: formatError(error) }], isError: true };
20
50
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
2
3
  import { formatPaginatedAssets, formatError } from '../utils/format.js';
3
4
 
4
5
  const schema = {
@@ -7,14 +8,43 @@ const schema = {
7
8
  };
8
9
 
9
10
  export function registerListMemeHooks(server, client) {
10
- server.tool(
11
+ registerAppTool(
12
+ server,
11
13
  'list_meme_hooks',
12
- 'List available meme hook clips (2-5 second attention grabbers prepended to shorts). Use the exact memeHookName when creating shorts.',
13
- schema,
14
+ {
15
+ title: 'List Meme Hooks',
16
+ description:
17
+ 'List available meme hook clips (2-5 second attention grabbers prepended to shorts). Use the exact memeHookName when creating shorts.',
18
+ inputSchema: schema,
19
+ _meta: {
20
+ ui: { resourceUri: 'ui://ssemble/meme-hooks-app.html' },
21
+ },
22
+ },
14
23
  async (params) => {
15
24
  try {
16
25
  const result = await client.listMemeHooks(params);
17
- return { content: [{ type: 'text', text: formatPaginatedAssets(result, 'memeHooks', 'Meme Hooks') }] };
26
+ const textFallback = formatPaginatedAssets(result, 'memeHooks', 'Meme Hooks');
27
+
28
+ const items = result.data?.memeHooks || [];
29
+ const pagination = result.data?.pagination || {};
30
+ const structuredContent = {
31
+ memeHooks: items.map((h) => ({
32
+ name: h.name,
33
+ preview: h.preview || '',
34
+ video: h.video || '',
35
+ video_duration: h.video_duration || 0,
36
+ })),
37
+ pagination: {
38
+ page: pagination.page || 1,
39
+ totalPages: pagination.totalPages || 1,
40
+ totalCount: pagination.totalCount || items.length,
41
+ },
42
+ };
43
+
44
+ return {
45
+ content: [{ type: 'text', text: textFallback }],
46
+ structuredContent,
47
+ };
18
48
  } catch (error) {
19
49
  return { content: [{ type: 'text', text: formatError(error) }], isError: true };
20
50
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
2
3
  import { formatPaginatedAssets, formatError } from '../utils/format.js';
3
4
 
4
5
  const schema = {
@@ -7,14 +8,43 @@ const schema = {
7
8
  };
8
9
 
9
10
  export function registerListMusic(server, client) {
10
- server.tool(
11
+ registerAppTool(
12
+ server,
11
13
  'list_music',
12
- 'List available background music tracks with names and durations. Use the exact musicName when creating shorts.',
13
- schema,
14
+ {
15
+ title: 'List Music',
16
+ description:
17
+ 'List available background music tracks with names and durations. Use the exact musicName when creating shorts.',
18
+ inputSchema: schema,
19
+ _meta: {
20
+ ui: { resourceUri: 'ui://ssemble/music-app.html' },
21
+ },
22
+ },
14
23
  async (params) => {
15
24
  try {
16
25
  const result = await client.listMusic(params);
17
- return { content: [{ type: 'text', text: formatPaginatedAssets(result, 'music', 'Background Music') }] };
26
+ const textFallback = formatPaginatedAssets(result, 'music', 'Background Music');
27
+
28
+ // Structured content for the MCP App
29
+ const items = result.data?.music || [];
30
+ const pagination = result.data?.pagination || {};
31
+ const structuredContent = {
32
+ music: items.map((t) => ({
33
+ name: t.name,
34
+ duration: t.duration,
35
+ url: t.url || '',
36
+ })),
37
+ pagination: {
38
+ page: pagination.page || 1,
39
+ totalPages: pagination.totalPages || 1,
40
+ totalCount: pagination.totalCount || items.length,
41
+ },
42
+ };
43
+
44
+ return {
45
+ content: [{ type: 'text', text: textFallback }],
46
+ structuredContent,
47
+ };
18
48
  } catch (error) {
19
49
  return { content: [{ type: 'text', text: formatError(error) }], isError: true };
20
50
  }