@ssemble/mcp-server 1.0.5 → 1.1.0
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.
- package/README.md +29 -1
- package/package.json +9 -2
- package/server.json +3 -3
- package/src/apps/dist/game-videos-app.html +163 -0
- package/src/apps/dist/meme-hooks-app.html +165 -0
- package/src/apps/dist/music-app.html +174 -0
- package/src/apps/dist/shorts-app.html +218 -0
- package/src/apps/dist/templates-app.html +157 -0
- package/src/apps/game-videos-app.html +92 -0
- package/src/apps/game-videos-app.js +102 -0
- package/src/apps/meme-hooks-app.html +95 -0
- package/src/apps/meme-hooks-app.js +94 -0
- package/src/apps/music-app.html +96 -0
- package/src/apps/music-app.js +151 -0
- package/src/apps/shorts-app.html +135 -0
- package/src/apps/shorts-app.js +102 -0
- package/src/apps/templates-app.html +87 -0
- package/src/apps/templates-app.js +74 -0
- package/src/assets/thumbnails/ali.jpg +0 -0
- package/src/assets/thumbnails/beast.jpg +0 -0
- package/src/assets/thumbnails/box.jpg +0 -0
- package/src/assets/thumbnails/hormozi1.jpg +0 -0
- package/src/assets/thumbnails/hormozi2.jpg +0 -0
- package/src/assets/thumbnails/iman.jpg +0 -0
- package/src/assets/thumbnails/iman2.jpg +0 -0
- package/src/assets/thumbnails/karaoke.jpg +0 -0
- package/src/assets/thumbnails/simple.jpg +0 -0
- package/src/index.js +2 -0
- package/src/resources/app-resources.js +149 -0
- package/src/tools/create-short.js +1 -0
- package/src/tools/get-shorts.js +36 -4
- package/src/tools/list-game-videos.js +34 -4
- package/src/tools/list-meme-hooks.js +34 -4
- package/src/tools/list-music.js +34 -4
- package/src/tools/list-templates.js +64 -5
- package/src/utils/format.js +8 -4
|
@@ -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
|
|
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
|
+
}
|
|
@@ -28,6 +28,7 @@ const schema = {
|
|
|
28
28
|
musicName: z.string().describe('Exact track name from list_music (case-sensitive)').optional(),
|
|
29
29
|
musicVolume: z.number().min(0).max(100).default(10).describe('Music volume 0-100').optional(),
|
|
30
30
|
layout: z.enum(['auto', 'fill', 'fit', 'square']).default('auto').describe('Video framing layout').optional(),
|
|
31
|
+
webhookUrl: z.string().url().describe('Optional webhook URL for completion/failure notifications. Receives a POST with results when processing finishes.').optional(),
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
export function registerCreateShort(server, client) {
|
package/src/tools/get-shorts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
|
|
2
3
|
import { formatShortsResponse, formatError } from '../utils/format.js';
|
|
3
4
|
|
|
4
5
|
const schema = {
|
|
@@ -6,14 +7,45 @@ const schema = {
|
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
export function registerGetShorts(server, client) {
|
|
9
|
-
|
|
10
|
+
registerAppTool(
|
|
11
|
+
server,
|
|
10
12
|
'get_shorts',
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
{
|
|
14
|
+
title: 'Get Shorts',
|
|
15
|
+
description:
|
|
16
|
+
'Retrieve all generated short clips for a completed request. Returns video URLs, AI-generated titles, descriptions, viral scores, and durations sorted by viral score.',
|
|
17
|
+
inputSchema: schema,
|
|
18
|
+
_meta: {
|
|
19
|
+
ui: { resourceUri: 'ui://ssemble/shorts-app.html' },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
13
22
|
async ({ requestId }) => {
|
|
14
23
|
try {
|
|
15
24
|
const result = await client.getShorts(requestId);
|
|
16
|
-
|
|
25
|
+
const textFallback = formatShortsResponse(result);
|
|
26
|
+
const d = result.data;
|
|
27
|
+
|
|
28
|
+
const structuredContent = {
|
|
29
|
+
title: d.title || '',
|
|
30
|
+
url: d.url || '',
|
|
31
|
+
start: d.start,
|
|
32
|
+
end: d.end,
|
|
33
|
+
language: d.language || '',
|
|
34
|
+
template: d.template || '',
|
|
35
|
+
shorts: (d.shorts || []).map((s) => ({
|
|
36
|
+
title: s.title || '',
|
|
37
|
+
description: s.description || '',
|
|
38
|
+
duration: s.duration || 0,
|
|
39
|
+
viral_score: s.viral_score != null ? s.viral_score : 0,
|
|
40
|
+
video_url: s.video_url || '',
|
|
41
|
+
error: !!s.error,
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: textFallback }],
|
|
47
|
+
structuredContent,
|
|
48
|
+
};
|
|
17
49
|
} catch (error) {
|
|
18
50
|
return { content: [{ type: 'text', text: formatError(error) }], isError: true };
|
|
19
51
|
}
|
|
@@ -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
|
-
|
|
11
|
+
registerAppTool(
|
|
12
|
+
server,
|
|
11
13
|
'list_game_videos',
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
registerAppTool(
|
|
12
|
+
server,
|
|
11
13
|
'list_meme_hooks',
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/tools/list-music.js
CHANGED
|
@@ -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
|
-
|
|
11
|
+
registerAppTool(
|
|
12
|
+
server,
|
|
11
13
|
'list_music',
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1,14 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { formatError, formatRateLimits } from '../utils/format.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const THUMB_DIR = join(__dirname, '..', 'assets', 'thumbnails');
|
|
9
|
+
|
|
10
|
+
function loadThumbnail(templateName) {
|
|
11
|
+
try {
|
|
12
|
+
const data = readFileSync(join(THUMB_DIR, `${templateName}.jpg`));
|
|
13
|
+
return { data: data.toString('base64'), mimeType: 'image/jpeg' };
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
export function registerListTemplates(server, client) {
|
|
4
|
-
|
|
20
|
+
registerAppTool(
|
|
21
|
+
server,
|
|
5
22
|
'list_templates',
|
|
6
|
-
|
|
7
|
-
|
|
23
|
+
{
|
|
24
|
+
title: 'List Templates',
|
|
25
|
+
description:
|
|
26
|
+
'List all available caption style templates with preview thumbnails, names, and IDs. Use the templateId when creating shorts.',
|
|
27
|
+
_meta: {
|
|
28
|
+
ui: { resourceUri: 'ui://ssemble/templates-app.html' },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
8
31
|
async () => {
|
|
9
32
|
try {
|
|
10
33
|
const result = await client.listTemplates();
|
|
11
|
-
|
|
34
|
+
const d = result.data;
|
|
35
|
+
const templates = d.templates || [];
|
|
36
|
+
|
|
37
|
+
// Text + image content for non-app clients (Claude Code, Cursor, etc.)
|
|
38
|
+
const content = [];
|
|
39
|
+
content.push({
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: `## Caption Templates (${templates.length} available)\n`,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
templates.forEach((t) => {
|
|
45
|
+
const isDefault = t.templateId === d.defaultTemplateId;
|
|
46
|
+
const thumb = loadThumbnail(t.name);
|
|
47
|
+
if (thumb) {
|
|
48
|
+
content.push({ type: 'image', data: thumb.data, mimeType: thumb.mimeType });
|
|
49
|
+
}
|
|
50
|
+
const label = `**${t.displayName || t.name}**${isDefault ? ' _(default)_' : ''}`;
|
|
51
|
+
content.push({ type: 'text', text: label });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let footer = `\nUse the \`templateId\` field when calling \`create_short\` to pick a style.`;
|
|
55
|
+
footer += formatRateLimits(result.rateLimits);
|
|
56
|
+
content.push({ type: 'text', text: footer });
|
|
57
|
+
|
|
58
|
+
// Structured content for the MCP App
|
|
59
|
+
const structuredContent = {
|
|
60
|
+
templates: templates.map((t) => ({
|
|
61
|
+
templateId: t.templateId,
|
|
62
|
+
name: t.name,
|
|
63
|
+
displayName: t.displayName || t.name,
|
|
64
|
+
gif: t.gif || '',
|
|
65
|
+
preview: t.preview || '',
|
|
66
|
+
})),
|
|
67
|
+
defaultTemplateId: d.defaultTemplateId,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return { content, structuredContent };
|
|
12
71
|
} catch (error) {
|
|
13
72
|
return { content: [{ type: 'text', text: formatError(error) }], isError: true };
|
|
14
73
|
}
|
package/src/utils/format.js
CHANGED
|
@@ -144,14 +144,18 @@ export function formatPaginatedAssets(result, assetKey, assetLabel) {
|
|
|
144
144
|
return `No ${assetLabel} found.${formatRateLimits(result.rateLimits)}`;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
const hasUrl = items.some((item) => item.url);
|
|
148
|
+
const hasDuration = items[0]?.duration != null;
|
|
149
|
+
|
|
147
150
|
let text = `## ${assetLabel} (Page ${p.page}/${p.totalPages}, ${p.totalCount} total)\n\n`;
|
|
148
|
-
text += `| # | Name |${
|
|
149
|
-
text += `|---|------|${
|
|
151
|
+
text += `| # | Name |${hasDuration ? ' Duration |' : ''}${hasUrl ? ' Listen |' : ''}\n`;
|
|
152
|
+
text += `|---|------|${hasDuration ? '----------|' : ''}${hasUrl ? '--------|' : ''}\n`;
|
|
150
153
|
|
|
151
154
|
items.forEach((item, i) => {
|
|
152
155
|
const num = (p.page - 1) * p.limit + i + 1;
|
|
153
|
-
const durationCol =
|
|
154
|
-
|
|
156
|
+
const durationCol = hasDuration ? ` ${Math.round(item.duration)}s |` : '';
|
|
157
|
+
const listenCol = hasUrl ? ` ${item.url ? `[▶ Play](${item.url})` : ''} |` : '';
|
|
158
|
+
text += `| ${num} | ${item.name} |${durationCol}${listenCol}\n`;
|
|
155
159
|
});
|
|
156
160
|
|
|
157
161
|
if (p.totalPages > 1) {
|