@visionengine/remotion-file-bridge 1.0.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-zh.md +31 -0
- package/README.md +31 -0
- package/dist/client.js +187 -0
- package/dist/index.js +3 -0
- package/dist/path-mapping.js +33 -0
- package/dist/server.js +62 -0
- package/dist/shared.js +82 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
package/README-zh.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# VE Remotion 文件桥接 MCP
|
|
2
|
+
|
|
3
|
+
用于通过 `ve-backend` 查询、读取、下载、保存 Remotion 项目文件的 MCP 服务。
|
|
4
|
+
|
|
5
|
+
## 环境变量
|
|
6
|
+
|
|
7
|
+
参考 `.env.example`。
|
|
8
|
+
|
|
9
|
+
必填:
|
|
10
|
+
|
|
11
|
+
- `BASE_URL`
|
|
12
|
+
- `API_KEY`
|
|
13
|
+
- `WORKDIR`
|
|
14
|
+
|
|
15
|
+
其中 `WORKDIR` 表示本地工作目录:`download_file` 下载文件到本地时,以及 `save_file` 读取相对本地路径时,都会以它为基准。
|
|
16
|
+
|
|
17
|
+
## 工具
|
|
18
|
+
|
|
19
|
+
- `list_files`:按 `children` / `tree` / `file` 模式浏览当前用户工作区。
|
|
20
|
+
- `read_file`:直接读取文本文件,或返回二进制资源的预览/下载地址。
|
|
21
|
+
- `download_file`:把远端工作区文件下载到本地 `WORKDIR`。
|
|
22
|
+
- `save_file`:把本地文件、文本、base64 内容或远端 URL 保存到工作区。
|
|
23
|
+
|
|
24
|
+
## 后端依赖
|
|
25
|
+
|
|
26
|
+
该 MCP 依赖以下后端接口:
|
|
27
|
+
|
|
28
|
+
- `GET /api/v1/auth/me`
|
|
29
|
+
- `GET /api/v1/files/list`
|
|
30
|
+
- `GET /shared/{userId}/{path}`
|
|
31
|
+
- `POST /save`
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# VE Remotion File Bridge
|
|
2
|
+
|
|
3
|
+
MCP server for listing, reading, downloading, and saving Remotion project files through `ve-backend`.
|
|
4
|
+
|
|
5
|
+
## Environment
|
|
6
|
+
|
|
7
|
+
See `.env.example`.
|
|
8
|
+
|
|
9
|
+
Required:
|
|
10
|
+
|
|
11
|
+
- `BASE_URL`
|
|
12
|
+
- `API_KEY`
|
|
13
|
+
- `WORKDIR`
|
|
14
|
+
|
|
15
|
+
`WORKDIR` is the local base directory used when `download_file` writes files locally or when `save_file` reads a relative local path.
|
|
16
|
+
|
|
17
|
+
## Tools
|
|
18
|
+
|
|
19
|
+
- `list_files`: browse the current user's Remotion workspace with `children` / `tree` / `file` modes.
|
|
20
|
+
- `read_file`: read text files directly, or return preview/download URLs for binary assets.
|
|
21
|
+
- `download_file`: download a remote workspace file into local `WORKDIR`.
|
|
22
|
+
- `save_file`: save local files, text, base64 content, or remote URLs into the workspace.
|
|
23
|
+
|
|
24
|
+
## Backend requirements
|
|
25
|
+
|
|
26
|
+
This MCP package depends on these backend endpoints:
|
|
27
|
+
|
|
28
|
+
- `GET /api/v1/auth/me`
|
|
29
|
+
- `GET /api/v1/files/list`
|
|
30
|
+
- `GET /shared/{userId}/{path}`
|
|
31
|
+
- `POST /save`
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { buildSharedDownloadUrl, buildSharedPreviewUrl, ensureDir, getBaseUrl, getWorkDir, normalizeWorkspacePath, requireApiKey, resolveUserId, toRelativePath } from './shared.js';
|
|
4
|
+
import { inferTargetPath } from './path-mapping.js';
|
|
5
|
+
const TEXT_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt', '.css', '.html', '.srt', '.ass', '.vtt', '.yml', '.yaml', '.csv']);
|
|
6
|
+
function isTextPath(filePath) {
|
|
7
|
+
return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
8
|
+
}
|
|
9
|
+
function buildAuthHeaders() {
|
|
10
|
+
return {
|
|
11
|
+
Authorization: `Bearer ${requireApiKey()}`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export async function listProjectFiles(args) {
|
|
15
|
+
const userId = await resolveUserId(args.userId);
|
|
16
|
+
const workspacePath = normalizeWorkspacePath(args.path || '.', userId);
|
|
17
|
+
const mode = args.mode || (args.recursive ? 'tree' : 'children');
|
|
18
|
+
const query = new URLSearchParams({
|
|
19
|
+
path: workspacePath,
|
|
20
|
+
mode,
|
|
21
|
+
max_depth: String(args.maxDepth ?? 2),
|
|
22
|
+
limit: String(args.limit ?? 200),
|
|
23
|
+
include_content: String(Boolean(args.includeContent)),
|
|
24
|
+
});
|
|
25
|
+
const response = await fetch(`${getBaseUrl()}/api/v1/files/list?${query.toString()}`, {
|
|
26
|
+
headers: buildAuthHeaders(),
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`Failed to list files: ${response.status} ${await response.text()}`);
|
|
30
|
+
}
|
|
31
|
+
const payload = await response.json();
|
|
32
|
+
return {
|
|
33
|
+
success: payload.success,
|
|
34
|
+
userId,
|
|
35
|
+
workspacePath: payload.path,
|
|
36
|
+
metadata: {
|
|
37
|
+
mode: payload.mode,
|
|
38
|
+
pagination: payload.pagination,
|
|
39
|
+
entries: payload.entries,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export async function readProjectFile(args) {
|
|
44
|
+
const userId = await resolveUserId(args.userId);
|
|
45
|
+
const workspacePath = normalizeWorkspacePath(args.path, userId);
|
|
46
|
+
const previewUrl = buildSharedPreviewUrl(userId, workspacePath);
|
|
47
|
+
const downloadUrl = buildSharedDownloadUrl(userId, workspacePath);
|
|
48
|
+
const shouldReadText = args.asText !== false && isTextPath(workspacePath);
|
|
49
|
+
if (!shouldReadText) {
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
userId,
|
|
53
|
+
workspacePath,
|
|
54
|
+
previewUrl,
|
|
55
|
+
downloadUrl,
|
|
56
|
+
metadata: {
|
|
57
|
+
mode: args.download ? 'download' : 'preview',
|
|
58
|
+
contentKind: 'binary',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const response = await fetch(args.download ? downloadUrl : previewUrl);
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`Failed to read file: ${response.status} ${await response.text()}`);
|
|
65
|
+
}
|
|
66
|
+
const content = await response.text();
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
userId,
|
|
70
|
+
workspacePath,
|
|
71
|
+
previewUrl,
|
|
72
|
+
downloadUrl,
|
|
73
|
+
metadata: {
|
|
74
|
+
contentKind: 'text',
|
|
75
|
+
content,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export async function downloadProjectFile(args) {
|
|
80
|
+
const userId = await resolveUserId(args.userId);
|
|
81
|
+
const workspacePath = normalizeWorkspacePath(args.remotePath, userId);
|
|
82
|
+
const downloadUrl = buildSharedDownloadUrl(userId, workspacePath);
|
|
83
|
+
const previewUrl = buildSharedPreviewUrl(userId, workspacePath);
|
|
84
|
+
const relativeOutput = args.localPath?.trim()
|
|
85
|
+
? args.localPath.trim().replaceAll('\\', '/')
|
|
86
|
+
: `${inferTargetPath(path.basename(workspacePath), undefined, undefined)}/${path.basename(workspacePath)}`;
|
|
87
|
+
const absoluteOutput = path.isAbsolute(relativeOutput)
|
|
88
|
+
? relativeOutput
|
|
89
|
+
: path.join(getWorkDir(), relativeOutput);
|
|
90
|
+
ensureDir(path.dirname(absoluteOutput));
|
|
91
|
+
const response = await fetch(downloadUrl);
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`Failed to download file: ${response.status} ${await response.text()}`);
|
|
94
|
+
}
|
|
95
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
96
|
+
await fs.promises.writeFile(absoluteOutput, Buffer.from(arrayBuffer));
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
userId,
|
|
100
|
+
workspacePath,
|
|
101
|
+
previewUrl,
|
|
102
|
+
downloadUrl,
|
|
103
|
+
localPath: toRelativePath(absoluteOutput),
|
|
104
|
+
metadata: {
|
|
105
|
+
size: Buffer.byteLength(Buffer.from(arrayBuffer)),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export async function saveProjectFile(args) {
|
|
110
|
+
const userId = await resolveUserId(args.userId);
|
|
111
|
+
const targetPath = inferTargetPath(args.fileName, args.contentType, args.targetPath);
|
|
112
|
+
const saveUrl = `${getBaseUrl()}/save`;
|
|
113
|
+
let response;
|
|
114
|
+
if (args.sourceType === 'local_path') {
|
|
115
|
+
if (!args.localPath) {
|
|
116
|
+
throw new Error('localPath is required when sourceType=local_path');
|
|
117
|
+
}
|
|
118
|
+
const absolutePath = path.isAbsolute(args.localPath) ? args.localPath : path.join(getWorkDir(), args.localPath);
|
|
119
|
+
const fileBuffer = await fs.promises.readFile(absolutePath);
|
|
120
|
+
const form = new FormData();
|
|
121
|
+
form.append('file', new Blob([fileBuffer], { type: args.contentType || 'application/octet-stream' }), args.fileName);
|
|
122
|
+
form.append('file_name', args.fileName);
|
|
123
|
+
form.append('path', targetPath);
|
|
124
|
+
response = await fetch(saveUrl, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: buildAuthHeaders(),
|
|
127
|
+
body: form,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else if (args.sourceType === 'remote_url') {
|
|
131
|
+
if (!args.content) {
|
|
132
|
+
throw new Error('content must be the remote URL when sourceType=remote_url');
|
|
133
|
+
}
|
|
134
|
+
const upstream = await fetch(args.content);
|
|
135
|
+
if (!upstream.ok) {
|
|
136
|
+
throw new Error(`Failed to fetch remote_url source: ${upstream.status} ${await upstream.text()}`);
|
|
137
|
+
}
|
|
138
|
+
const buffer = Buffer.from(await upstream.arrayBuffer());
|
|
139
|
+
const form = new FormData();
|
|
140
|
+
form.append('file', new Blob([buffer], { type: args.contentType || upstream.headers.get('content-type') || 'application/octet-stream' }), args.fileName);
|
|
141
|
+
form.append('file_name', args.fileName);
|
|
142
|
+
form.append('path', targetPath);
|
|
143
|
+
response = await fetch(saveUrl, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: buildAuthHeaders(),
|
|
146
|
+
body: form,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (typeof args.content !== 'string') {
|
|
151
|
+
throw new Error('content is required for text/base64 source types');
|
|
152
|
+
}
|
|
153
|
+
response = await fetch(saveUrl, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: {
|
|
156
|
+
...buildAuthHeaders(),
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
file_name: args.fileName,
|
|
161
|
+
content: args.content,
|
|
162
|
+
path: targetPath,
|
|
163
|
+
encoding: args.sourceType === 'text' ? 'text' : 'base64',
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`Failed to save file: ${response.status} ${await response.text()}`);
|
|
169
|
+
}
|
|
170
|
+
const payload = await response.json();
|
|
171
|
+
const returnedPath = payload.file.path.replaceAll('\\', '/');
|
|
172
|
+
if (!returnedPath.startsWith(`${userId}/`)) {
|
|
173
|
+
throw new Error(`Saved file belongs to another user workspace: ${returnedPath}`);
|
|
174
|
+
}
|
|
175
|
+
const workspacePath = normalizeWorkspacePath(returnedPath, userId);
|
|
176
|
+
return {
|
|
177
|
+
success: payload.success,
|
|
178
|
+
userId,
|
|
179
|
+
workspacePath,
|
|
180
|
+
previewUrl: buildSharedPreviewUrl(userId, workspacePath),
|
|
181
|
+
downloadUrl: buildSharedDownloadUrl(userId, workspacePath),
|
|
182
|
+
metadata: {
|
|
183
|
+
file: payload.file,
|
|
184
|
+
overwriteRequested: Boolean(args.overwrite),
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.avi', '.mkv', '.webm']);
|
|
3
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg']);
|
|
4
|
+
const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.aac', '.m4a', '.flac', '.ogg']);
|
|
5
|
+
const FONT_EXTENSIONS = new Set(['.ttf', '.otf', '.woff', '.woff2']);
|
|
6
|
+
const DATA_EXTENSIONS = new Set(['.json', '.csv', '.srt', '.ass', '.vtt']);
|
|
7
|
+
const DOC_EXTENSIONS = new Set(['.md', '.txt', '.pdf', '.doc', '.docx']);
|
|
8
|
+
const SOURCE_EXTENSIONS = new Set(['.tsx', '.ts', '.jsx', '.js', '.css', '.html']);
|
|
9
|
+
function normalizeDir(value) {
|
|
10
|
+
return value.replaceAll('\\', '/').replace(/^\/+/, '').replace(/\/+/g, '/');
|
|
11
|
+
}
|
|
12
|
+
export function inferTargetPath(fileName, contentType, explicitPath) {
|
|
13
|
+
if (explicitPath?.trim()) {
|
|
14
|
+
return normalizeDir(explicitPath.trim());
|
|
15
|
+
}
|
|
16
|
+
const normalizedType = (contentType || '').toLowerCase();
|
|
17
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
18
|
+
if (normalizedType.startsWith('video/') || VIDEO_EXTENSIONS.has(ext))
|
|
19
|
+
return 'public/videos';
|
|
20
|
+
if (normalizedType.startsWith('image/') || IMAGE_EXTENSIONS.has(ext))
|
|
21
|
+
return 'public/images';
|
|
22
|
+
if (normalizedType.startsWith('audio/') || AUDIO_EXTENSIONS.has(ext))
|
|
23
|
+
return 'public/audio';
|
|
24
|
+
if (normalizedType.includes('font') || FONT_EXTENSIONS.has(ext))
|
|
25
|
+
return 'public/fonts';
|
|
26
|
+
if (DATA_EXTENSIONS.has(ext))
|
|
27
|
+
return 'public/data';
|
|
28
|
+
if (DOC_EXTENSIONS.has(ext))
|
|
29
|
+
return 'public/documents';
|
|
30
|
+
if (SOURCE_EXTENSIONS.has(ext))
|
|
31
|
+
return 'src';
|
|
32
|
+
return 'public/documents';
|
|
33
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { FastMCP } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { downloadProjectFile, listProjectFiles, readProjectFile, saveProjectFile } from './client.js';
|
|
4
|
+
const server = new FastMCP({
|
|
5
|
+
name: 'VE Remotion File Bridge',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
});
|
|
8
|
+
server.addTool({
|
|
9
|
+
name: 'list_files',
|
|
10
|
+
description: 'List files in the current Remotion project workspace via the backend file bridge API.',
|
|
11
|
+
annotations: { title: 'List Remotion Project Files', readOnlyHint: true, openWorldHint: false },
|
|
12
|
+
parameters: z.object({
|
|
13
|
+
userId: z.string().optional().describe('Optional user id override. Normally resolved automatically from API key.'),
|
|
14
|
+
path: z.string().optional().describe('Workspace-relative path, default is root.'),
|
|
15
|
+
recursive: z.boolean().optional().describe('Whether to recursively browse files. If true, defaults mode to tree.'),
|
|
16
|
+
mode: z.enum(['children', 'tree', 'file']).optional(),
|
|
17
|
+
includeContent: z.boolean().optional(),
|
|
18
|
+
maxDepth: z.number().int().min(1).max(10).optional(),
|
|
19
|
+
limit: z.number().int().min(1).max(1000).optional(),
|
|
20
|
+
}),
|
|
21
|
+
execute: async (args) => JSON.stringify(await listProjectFiles(args), null, 2),
|
|
22
|
+
});
|
|
23
|
+
server.addTool({
|
|
24
|
+
name: 'read_file',
|
|
25
|
+
description: 'Read a project file as text when suitable, or return preview/download URLs for binary files.',
|
|
26
|
+
annotations: { title: 'Read Remotion Project File', readOnlyHint: true, openWorldHint: false },
|
|
27
|
+
parameters: z.object({
|
|
28
|
+
userId: z.string().optional(),
|
|
29
|
+
path: z.string().describe('Workspace-relative file path.'),
|
|
30
|
+
asText: z.boolean().optional().describe('Whether to fetch text content for text-like files. Defaults to true.'),
|
|
31
|
+
download: z.boolean().optional().describe('Whether to prefer download mode when requesting remote content.'),
|
|
32
|
+
}),
|
|
33
|
+
execute: async (args) => JSON.stringify(await readProjectFile(args), null, 2),
|
|
34
|
+
});
|
|
35
|
+
server.addTool({
|
|
36
|
+
name: 'download_file',
|
|
37
|
+
description: 'Download a remote workspace file into the local WORKDIR.',
|
|
38
|
+
annotations: { title: 'Download Remotion Project File', readOnlyHint: false, openWorldHint: false },
|
|
39
|
+
parameters: z.object({
|
|
40
|
+
userId: z.string().optional(),
|
|
41
|
+
remotePath: z.string().describe('Workspace-relative remote file path.'),
|
|
42
|
+
localPath: z.string().optional().describe('Optional local output path, relative to WORKDIR or absolute.'),
|
|
43
|
+
}),
|
|
44
|
+
execute: async (args) => JSON.stringify(await downloadProjectFile(args), null, 2),
|
|
45
|
+
});
|
|
46
|
+
server.addTool({
|
|
47
|
+
name: 'save_file',
|
|
48
|
+
description: 'Save local files, text, base64 payloads, or remote URLs into the current user Remotion workspace.',
|
|
49
|
+
annotations: { title: 'Save Remotion Project File', readOnlyHint: false, openWorldHint: true },
|
|
50
|
+
parameters: z.object({
|
|
51
|
+
userId: z.string().optional(),
|
|
52
|
+
sourceType: z.enum(['local_path', 'text', 'base64', 'remote_url']),
|
|
53
|
+
localPath: z.string().optional(),
|
|
54
|
+
content: z.string().optional(),
|
|
55
|
+
fileName: z.string().min(1),
|
|
56
|
+
targetPath: z.string().optional(),
|
|
57
|
+
contentType: z.string().optional(),
|
|
58
|
+
overwrite: z.boolean().optional(),
|
|
59
|
+
}),
|
|
60
|
+
execute: async (args) => JSON.stringify(await saveProjectFile(args), null, 2),
|
|
61
|
+
});
|
|
62
|
+
export { server };
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const BASE_URL = (process.env.BASE_URL || 'https://api.visionengine-tech.com').replace(/\/$/, '');
|
|
4
|
+
const WORKDIR = process.env.WORKDIR || './';
|
|
5
|
+
let cachedUserId = null;
|
|
6
|
+
export function getBaseUrl() {
|
|
7
|
+
return BASE_URL;
|
|
8
|
+
}
|
|
9
|
+
export function getWorkDir() {
|
|
10
|
+
return path.isAbsolute(WORKDIR) ? WORKDIR : path.resolve(process.cwd(), WORKDIR);
|
|
11
|
+
}
|
|
12
|
+
export function ensureDir(dirPath) {
|
|
13
|
+
if (!fs.existsSync(dirPath)) {
|
|
14
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
return dirPath;
|
|
17
|
+
}
|
|
18
|
+
export function toRelativePath(absolutePath) {
|
|
19
|
+
const workDir = getWorkDir();
|
|
20
|
+
if (absolutePath.startsWith(workDir)) {
|
|
21
|
+
return path.relative(workDir, absolutePath).replaceAll('\\', '/') || '.';
|
|
22
|
+
}
|
|
23
|
+
return absolutePath.replaceAll('\\', '/');
|
|
24
|
+
}
|
|
25
|
+
export function normalizeWorkspacePath(value, userId) {
|
|
26
|
+
let normalized = value.trim().replaceAll('\\', '/').replace(/^\/+/, '');
|
|
27
|
+
if (!normalized || normalized === '.') {
|
|
28
|
+
return '.';
|
|
29
|
+
}
|
|
30
|
+
if (/^[a-zA-Z]:/.test(normalized) || normalized.startsWith('//')) {
|
|
31
|
+
throw new Error('Path must be workspace-relative');
|
|
32
|
+
}
|
|
33
|
+
const prefix = userId ? `${userId}/` : '';
|
|
34
|
+
if (prefix && normalized.startsWith(prefix)) {
|
|
35
|
+
normalized = normalized.slice(prefix.length);
|
|
36
|
+
}
|
|
37
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
38
|
+
if (parts.some((part) => part === '..')) {
|
|
39
|
+
throw new Error('Path traversal is not allowed');
|
|
40
|
+
}
|
|
41
|
+
return parts.join('/');
|
|
42
|
+
}
|
|
43
|
+
export function buildSharedPreviewUrl(userId, workspacePath) {
|
|
44
|
+
const normalized = normalizeWorkspacePath(workspacePath, userId);
|
|
45
|
+
return `${BASE_URL}/shared/${userId}/${normalized}`;
|
|
46
|
+
}
|
|
47
|
+
export function buildSharedDownloadUrl(userId, workspacePath) {
|
|
48
|
+
return `${buildSharedPreviewUrl(userId, workspacePath)}?download=true`;
|
|
49
|
+
}
|
|
50
|
+
export function requireApiKey() {
|
|
51
|
+
const apiKey = process.env.API_KEY || '';
|
|
52
|
+
if (!apiKey) {
|
|
53
|
+
throw new Error('API_KEY environment variable is required');
|
|
54
|
+
}
|
|
55
|
+
return apiKey;
|
|
56
|
+
}
|
|
57
|
+
export async function fetchCurrentUserIdByApiKey() {
|
|
58
|
+
const apiKey = requireApiKey();
|
|
59
|
+
const response = await fetch(`${BASE_URL}/api/v1/auth/me`, {
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${apiKey}`,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`Failed to resolve user id: ${response.status} ${await response.text()}`);
|
|
66
|
+
}
|
|
67
|
+
const payload = await response.json();
|
|
68
|
+
if (!payload.success || !payload.user_id) {
|
|
69
|
+
throw new Error('Backend did not return a valid user_id');
|
|
70
|
+
}
|
|
71
|
+
cachedUserId = payload.user_id;
|
|
72
|
+
return payload.user_id;
|
|
73
|
+
}
|
|
74
|
+
export async function resolveUserId(inputUserId) {
|
|
75
|
+
if (inputUserId?.trim()) {
|
|
76
|
+
return inputUserId.trim();
|
|
77
|
+
}
|
|
78
|
+
if (cachedUserId) {
|
|
79
|
+
return cachedUserId;
|
|
80
|
+
}
|
|
81
|
+
return fetchCurrentUserIdByApiKey();
|
|
82
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@visionengine/remotion-file-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "VisionEngine Remotion File Bridge MCP Server",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ve-remotion-file-bridge": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"README-zh.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"clear": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"remotion",
|
|
25
|
+
"files",
|
|
26
|
+
"visionengine"
|
|
27
|
+
],
|
|
28
|
+
"author": "team@visionengine-tech.com",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/crazyyanchao/ve-mcp.git",
|
|
33
|
+
"directory": "packages/remotion-file-bridge"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://visionengine-tech.com/mcp",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"fastmcp": "^3.26.8",
|
|
38
|
+
"zod": "^4.1.12"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^24.10.1",
|
|
42
|
+
"typescript": "^5.8.3",
|
|
43
|
+
"vitest": "^3.1.3"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|