@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
@@ -1,14 +1,73 @@
1
- import { formatTemplatesResponse, formatError } from '../utils/format.js';
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
- server.tool(
20
+ registerAppTool(
21
+ server,
5
22
  'list_templates',
6
- 'List all available caption style templates with IDs and names. Use the templateId when creating shorts.',
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
- return { content: [{ type: 'text', text: formatTemplatesResponse(result) }] };
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
  }
@@ -99,23 +99,51 @@ export function formatListRequestsResponse(result) {
99
99
  }
100
100
 
101
101
  let text = `## Your Requests (Page ${p.page}/${p.totalPages}, ${p.totalCount} total)\n\n`;
102
- text += `| # | Request ID | Status | Progress | Created | Duration |\n`;
103
- text += `|---|-----------|--------|----------|---------|----------|\n`;
104
102
 
105
103
  requests.forEach((req, i) => {
104
+ const num = (p.page - 1) * p.limit + i + 1;
106
105
  const created = req.createdAt ? new Date(req.createdAt).toLocaleDateString() : 'N/A';
107
- const duration = req.duration ? `${req.duration}s` : 'N/A';
108
- text += `| ${(p.page - 1) * p.limit + i + 1} | \`${req.requestId}\` | ${req.status} | ${req.progress || 0}% | ${created} | ${duration} |\n`;
106
+ const label = req.title || req.url || req.fileUrl || 'Unknown source';
107
+ const short = label.length > 60 ? label.substring(0, 57) + '...' : label;
108
+ text += `**${num}. ${short}**\n`;
109
+ text += ` ID: \`${req.requestId}\` | Status: ${req.status} | Progress: ${req.progress || 0}% | Created: ${created}\n\n`;
109
110
  });
110
111
 
111
112
  if (p.totalPages > 1) {
112
- text += `\nPage ${p.page} of ${p.totalPages}. Use \`page\` parameter to browse more.`;
113
+ text += `Page ${p.page} of ${p.totalPages}. Use \`page\` parameter to browse more.`;
113
114
  }
114
115
 
115
116
  text += formatRateLimits(result.rateLimits);
116
117
  return text;
117
118
  }
118
119
 
120
+ /**
121
+ * Formats recent requests for the "no requestId given" UX.
122
+ * Used by get_status and get_shorts when called without a requestId.
123
+ * @param {Array} requests - from session store or API list_requests
124
+ * @param {string} tool - 'get_status' or 'get_shorts'
125
+ * @param {boolean} fromApi - true if coming from list_requests API (different field names)
126
+ */
127
+ export function formatRecentRequests(requests, tool, fromApi = false) {
128
+ if (requests.length === 0) {
129
+ return `No recent requests found. Use \`create_short\` to create one.`;
130
+ }
131
+
132
+ let text = `## Your Recent Requests\n\nWhich request would you like to check?\n\n`;
133
+
134
+ requests.forEach((req, i) => {
135
+ const id = fromApi ? req.requestId : req.requestId;
136
+ const label = req.title || req.url || req.fileUrl || 'Unknown source';
137
+ const short = label.length > 70 ? label.substring(0, 67) + '...' : label;
138
+ const status = req.status || 'unknown';
139
+ text += `**${i + 1}. ${short}**\n`;
140
+ text += ` ID: \`${id}\` | Status: ${status}\n\n`;
141
+ });
142
+
143
+ text += `Reply with the number or paste the request ID, and I'll call \`${tool}\` for you.`;
144
+ return text;
145
+ }
146
+
119
147
  export function formatTemplatesResponse(result) {
120
148
  const d = result.data;
121
149
  const templates = d.templates || [];
@@ -144,14 +172,18 @@ export function formatPaginatedAssets(result, assetKey, assetLabel) {
144
172
  return `No ${assetLabel} found.${formatRateLimits(result.rateLimits)}`;
145
173
  }
146
174
 
175
+ const hasUrl = items.some((item) => item.url);
176
+ const hasDuration = items[0]?.duration != null;
177
+
147
178
  let text = `## ${assetLabel} (Page ${p.page}/${p.totalPages}, ${p.totalCount} total)\n\n`;
148
- text += `| # | Name |${items[0]?.duration != null ? ' Duration |' : ''}\n`;
149
- text += `|---|------|${items[0]?.duration != null ? '----------|' : ''}\n`;
179
+ text += `| # | Name |${hasDuration ? ' Duration |' : ''}${hasUrl ? ' Listen |' : ''}\n`;
180
+ text += `|---|------|${hasDuration ? '----------|' : ''}${hasUrl ? '--------|' : ''}\n`;
150
181
 
151
182
  items.forEach((item, i) => {
152
183
  const num = (p.page - 1) * p.limit + i + 1;
153
- const durationCol = item.duration != null ? ` ${item.duration}s |` : '';
154
- text += `| ${num} | ${item.name} |${durationCol}\n`;
184
+ const durationCol = hasDuration ? ` ${Math.round(item.duration)}s |` : '';
185
+ const listenCol = hasUrl ? ` ${item.url ? `[▶ Play](${item.url})` : ''} |` : '';
186
+ text += `| ${num} | ${item.name} |${durationCol}${listenCol}\n`;
155
187
  });
156
188
 
157
189
  if (p.totalPages > 1) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * In-memory session store for request metadata.
3
+ * Maps requestId → { url, title, status, createdAt }
4
+ * Title is populated from get_shorts response once available.
5
+ * Lost when the MCP server process restarts (by design — use list_requests for history).
6
+ */
7
+
8
+ const store = new Map();
9
+
10
+ export function saveRequest(requestId, meta) {
11
+ store.set(requestId, { ...meta, savedAt: Date.now() });
12
+ }
13
+
14
+ export function updateRequest(requestId, updates) {
15
+ const existing = store.get(requestId);
16
+ if (existing) {
17
+ store.set(requestId, { ...existing, ...updates });
18
+ }
19
+ }
20
+
21
+ export function getRecentRequests(limit = 5) {
22
+ return [...store.entries()]
23
+ .sort((a, b) => b[1].savedAt - a[1].savedAt)
24
+ .slice(0, limit)
25
+ .map(([requestId, meta]) => ({ requestId, ...meta }));
26
+ }
27
+
28
+ export function hasRequests() {
29
+ return store.size > 0;
30
+ }