perso-mcp-server 1.0.0 → 1.0.2
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/dist/client.d.ts +5 -0
- package/dist/client.js +24 -0
- package/dist/index.js +9 -7
- package/dist/tools/files.d.ts +3 -2
- package/dist/tools/files.js +156 -17
- package/dist/tools/lip-sync.d.ts +2 -1
- package/dist/tools/lip-sync.js +15 -7
- package/dist/tools/projects.d.ts +2 -1
- package/dist/tools/projects.js +39 -19
- package/dist/tools/scripts.d.ts +2 -1
- package/dist/tools/scripts.js +15 -7
- package/dist/tools/spaces.d.ts +2 -1
- package/dist/tools/spaces.js +19 -1
- package/dist/tools/usage.d.ts +2 -1
- package/dist/tools/usage.js +8 -4
- package/dist/types.d.ts +11 -0
- package/dist/utils/space-resolver.d.ts +20 -0
- package/dist/utils/space-resolver.js +38 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -15,5 +15,10 @@ export declare class PersoApiClient {
|
|
|
15
15
|
put<T>(path: string, body?: unknown, query?: Record<string, string>): Promise<T>;
|
|
16
16
|
patch<T>(path: string, body?: unknown, query?: Record<string, string>): Promise<T>;
|
|
17
17
|
delete(path: string, query?: Record<string, string>): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Upload a file buffer directly to Azure Blob Storage via SAS URL.
|
|
20
|
+
* This bypasses the PERSO API — the SAS URL already contains authentication.
|
|
21
|
+
*/
|
|
22
|
+
static uploadBlob(blobSasUrl: string, fileBuffer: Buffer): Promise<string>;
|
|
18
23
|
}
|
|
19
24
|
export declare function createClient(): PersoApiClient;
|
package/dist/client.js
CHANGED
|
@@ -81,6 +81,30 @@ export class PersoApiClient {
|
|
|
81
81
|
async delete(path, query) {
|
|
82
82
|
return this.request("DELETE", path, undefined, query);
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Upload a file buffer directly to Azure Blob Storage via SAS URL.
|
|
86
|
+
* This bypasses the PERSO API — the SAS URL already contains authentication.
|
|
87
|
+
*/
|
|
88
|
+
static async uploadBlob(blobSasUrl, fileBuffer) {
|
|
89
|
+
const response = await fetch(blobSasUrl, {
|
|
90
|
+
method: "PUT",
|
|
91
|
+
headers: {
|
|
92
|
+
"x-ms-blob-type": "BlockBlob",
|
|
93
|
+
"Content-Type": "application/octet-stream",
|
|
94
|
+
"Content-Length": String(fileBuffer.byteLength),
|
|
95
|
+
},
|
|
96
|
+
body: fileBuffer,
|
|
97
|
+
});
|
|
98
|
+
if (response.status === 403) {
|
|
99
|
+
throw new PersoApiError("SAS_EXPIRED", 403, "SAS URL has expired. Call get_sas_token again to obtain a new URL.");
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new PersoApiError("BLOB_UPLOAD_FAILED", response.status, `Azure Blob upload failed with HTTP ${response.status}`);
|
|
103
|
+
}
|
|
104
|
+
// Extract blob URL (path portion before the '?' query string)
|
|
105
|
+
const url = new URL(blobSasUrl);
|
|
106
|
+
return `${url.origin}${url.pathname}`;
|
|
107
|
+
}
|
|
84
108
|
}
|
|
85
109
|
export function createClient() {
|
|
86
110
|
const apiKey = process.env.PERSO_API_KEY;
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { createClient } from "./client.js";
|
|
5
|
+
import { SpaceResolver } from "./utils/space-resolver.js";
|
|
5
6
|
import { registerSpaceTools } from "./tools/spaces.js";
|
|
6
7
|
import { registerFileTools } from "./tools/files.js";
|
|
7
8
|
import { registerProjectTools } from "./tools/projects.js";
|
|
@@ -13,18 +14,19 @@ import { registerLipSyncTools } from "./tools/lip-sync.js";
|
|
|
13
14
|
import { registerFeedbackTools } from "./tools/feedback.js";
|
|
14
15
|
async function main() {
|
|
15
16
|
const client = createClient();
|
|
17
|
+
const resolver = new SpaceResolver(client);
|
|
16
18
|
const server = new McpServer({
|
|
17
19
|
name: "perso-mcp-server",
|
|
18
|
-
version: "1.0.
|
|
20
|
+
version: "1.0.1",
|
|
19
21
|
});
|
|
20
|
-
registerSpaceTools(server, client);
|
|
21
|
-
registerFileTools(server, client);
|
|
22
|
-
registerProjectTools(server, client);
|
|
23
|
-
registerScriptTools(server, client);
|
|
24
|
-
registerUsageTools(server, client);
|
|
22
|
+
registerSpaceTools(server, client, resolver);
|
|
23
|
+
registerFileTools(server, client, resolver);
|
|
24
|
+
registerProjectTools(server, client, resolver);
|
|
25
|
+
registerScriptTools(server, client, resolver);
|
|
26
|
+
registerUsageTools(server, client, resolver);
|
|
25
27
|
registerLanguageTools(server, client);
|
|
26
28
|
registerCommunityTools(server, client);
|
|
27
|
-
registerLipSyncTools(server, client);
|
|
29
|
+
registerLipSyncTools(server, client, resolver);
|
|
28
30
|
registerFeedbackTools(server, client);
|
|
29
31
|
const transport = new StdioServerTransport();
|
|
30
32
|
await server.connect(transport);
|
package/dist/tools/files.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import { PersoApiClient } from "../client.js";
|
|
3
|
+
import type { SpaceResolver } from "../utils/space-resolver.js";
|
|
4
|
+
export declare function registerFileTools(server: McpServer, client: PersoApiClient, resolver: SpaceResolver): void;
|
package/dist/tools/files.js
CHANGED
|
@@ -1,57 +1,196 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { z } from "zod";
|
|
3
|
+
import { PersoApiClient } from "../client.js";
|
|
2
4
|
import { errorResult, textResult } from "../utils/formatting.js";
|
|
3
|
-
export function registerFileTools(server, client) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export function registerFileTools(server, client, resolver) {
|
|
6
|
+
// ──────────────────────────────────────────────
|
|
7
|
+
// Direct File Upload Workflow (Step 1 of 3)
|
|
8
|
+
// ──────────────────────────────────────────────
|
|
9
|
+
server.tool("get_sas_token", [
|
|
10
|
+
"Get a temporary Azure Blob Storage SAS URL for direct file upload.",
|
|
11
|
+
"",
|
|
12
|
+
"** Direct File Upload Workflow **",
|
|
13
|
+
" Step 1: get_sas_token → obtain blobSasUrl + expiresOn",
|
|
14
|
+
" Step 2: upload_file_to_blob → upload local file binary to blobSasUrl",
|
|
15
|
+
" Step 3: upload_media (source='blob') → register the blob URL with PERSO",
|
|
16
|
+
].join("\n"), {
|
|
9
17
|
mediaType: z
|
|
10
18
|
.enum(["video", "audio"])
|
|
11
19
|
.describe("Type of media to upload"),
|
|
20
|
+
fileName: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe("File name with extension (e.g., 'my_video.mp4')"),
|
|
23
|
+
}, async ({ mediaType, fileName }) => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await client.post("/file/api/v1/upload/sas-token", { mediaType: mediaType.toUpperCase(), fileName });
|
|
26
|
+
return textResult([
|
|
27
|
+
`SAS Token obtained`,
|
|
28
|
+
`Blob SAS URL: ${result.blobSasUrl}`,
|
|
29
|
+
`Expires: ${result.expiresOn}`,
|
|
30
|
+
``,
|
|
31
|
+
`Next step: call upload_file_to_blob with this blobSasUrl and your local file path.`,
|
|
32
|
+
].join("\n"));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return errorResult(error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// ──────────────────────────────────────────────
|
|
39
|
+
// Direct File Upload Workflow (Step 2 of 3)
|
|
40
|
+
// ──────────────────────────────────────────────
|
|
41
|
+
server.tool("upload_file_to_blob", [
|
|
42
|
+
"Upload a local file to Azure Blob Storage using the SAS URL from get_sas_token.",
|
|
43
|
+
"Returns the blob URL (without query string) to use as fileUrl in upload_media.",
|
|
44
|
+
"",
|
|
45
|
+
"** Direct File Upload Workflow **",
|
|
46
|
+
" Step 1: get_sas_token → obtain blobSasUrl + expiresOn",
|
|
47
|
+
" Step 2: upload_file_to_blob → upload local file binary to blobSasUrl ← YOU ARE HERE",
|
|
48
|
+
" Step 3: upload_media (source='blob') → register the blob URL with PERSO",
|
|
49
|
+
"",
|
|
50
|
+
"Note: The SAS URL contains authentication, so no API key is needed.",
|
|
51
|
+
"A 403 response means the SAS URL expired — call get_sas_token again.",
|
|
52
|
+
].join("\n"), {
|
|
53
|
+
filePath: z
|
|
54
|
+
.string()
|
|
55
|
+
.describe("Absolute path to the local file to upload"),
|
|
56
|
+
blobSasUrl: z
|
|
57
|
+
.string()
|
|
58
|
+
.describe("The blobSasUrl obtained from get_sas_token"),
|
|
59
|
+
}, async ({ filePath, blobSasUrl }) => {
|
|
60
|
+
try {
|
|
61
|
+
const fileBuffer = await readFile(filePath);
|
|
62
|
+
const blobUrl = await PersoApiClient.uploadBlob(blobSasUrl, fileBuffer);
|
|
63
|
+
return textResult([
|
|
64
|
+
`File uploaded to blob storage successfully`,
|
|
65
|
+
`Blob URL: ${blobUrl}`,
|
|
66
|
+
`File size: ${fileBuffer.byteLength} bytes`,
|
|
67
|
+
``,
|
|
68
|
+
`Next step: call upload_media with source='blob' and fileUrl='${blobUrl}'`,
|
|
69
|
+
].join("\n"));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return errorResult(error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// ──────────────────────────────────────────────
|
|
76
|
+
// Validate Media (for both workflows)
|
|
77
|
+
// ──────────────────────────────────────────────
|
|
78
|
+
server.tool("validate_media", [
|
|
79
|
+
"Validate media file constraints (size, duration, resolution) before uploading.",
|
|
80
|
+
"Use this to check if a file or external URL meets PERSO requirements.",
|
|
81
|
+
"",
|
|
82
|
+
"For external platforms (YouTube, TikTok, Google Drive):",
|
|
83
|
+
" Step 1: get_external_metadata → preview media info",
|
|
84
|
+
" Step 2: validate_media ← YOU ARE HERE",
|
|
85
|
+
" Step 3: upload_media (source='external') → start import",
|
|
86
|
+
].join("\n"), {
|
|
87
|
+
fileName: z
|
|
88
|
+
.string()
|
|
89
|
+
.describe("File name with extension (e.g., 'video.mp4')"),
|
|
12
90
|
fileUrl: z.string().describe("Direct file URL or external platform URL"),
|
|
91
|
+
mediaType: z
|
|
92
|
+
.enum(["video", "audio"])
|
|
93
|
+
.describe("Type of media to validate"),
|
|
94
|
+
}, async ({ fileName, fileUrl, mediaType }) => {
|
|
95
|
+
try {
|
|
96
|
+
const result = await client.post("/file/api/v1/media/validate", {
|
|
97
|
+
fileName,
|
|
98
|
+
fileUrl,
|
|
99
|
+
mediaType: mediaType.toUpperCase(),
|
|
100
|
+
});
|
|
101
|
+
const lines = [`Validation passed`];
|
|
102
|
+
if (result.maxDurationMs)
|
|
103
|
+
lines.push(`Max duration: ${result.maxDurationMs}ms`);
|
|
104
|
+
if (result.maxSizeBytes)
|
|
105
|
+
lines.push(`Max size: ${result.maxSizeBytes} bytes`);
|
|
106
|
+
if (result.maxResolution)
|
|
107
|
+
lines.push(`Max resolution: ${result.maxResolution}`);
|
|
108
|
+
if (result.message)
|
|
109
|
+
lines.push(`Message: ${result.message}`);
|
|
110
|
+
return textResult(lines.join("\n"));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return errorResult(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// ──────────────────────────────────────────────
|
|
117
|
+
// Upload Media — final registration step
|
|
118
|
+
// ──────────────────────────────────────────────
|
|
119
|
+
server.tool("upload_media", [
|
|
120
|
+
"Register an uploaded file with PERSO. Returns mediaSeq for creating translations.",
|
|
121
|
+
"spaceSeq is optional — omit to use the default space.",
|
|
122
|
+
"",
|
|
123
|
+
"source='blob' — Use after uploading a local file via get_sas_token + upload_file_to_blob.",
|
|
124
|
+
" Provide the blob URL (returned by upload_file_to_blob) as fileUrl.",
|
|
125
|
+
"source='external' — Use for YouTube / TikTok / Google Drive URLs.",
|
|
126
|
+
" Optionally call get_external_metadata and validate_media first.",
|
|
127
|
+
].join("\n"), {
|
|
128
|
+
spaceSeq: z
|
|
129
|
+
.number()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Space ID (omit to use default space)"),
|
|
132
|
+
source: z
|
|
133
|
+
.enum(["blob", "external"])
|
|
134
|
+
.describe("'blob' for files uploaded via SAS URL. 'external' for YouTube/TikTok/Google Drive URL."),
|
|
135
|
+
mediaType: z
|
|
136
|
+
.enum(["video", "audio"])
|
|
137
|
+
.describe("Type of media to register"),
|
|
138
|
+
fileUrl: z
|
|
139
|
+
.string()
|
|
140
|
+
.describe("Blob URL (from upload_file_to_blob) or external platform URL"),
|
|
13
141
|
fileName: z
|
|
14
142
|
.string()
|
|
15
143
|
.describe("File name with extension (e.g., 'my_video.mp4')"),
|
|
16
144
|
}, async ({ spaceSeq, source, mediaType, fileUrl, fileName }) => {
|
|
17
145
|
try {
|
|
146
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
18
147
|
if (source === "external") {
|
|
19
|
-
const result = await client.put("/file/api/upload/video/external", { spaceSeq, fileUrl, fileName });
|
|
148
|
+
const result = await client.put("/file/api/upload/video/external", { spaceSeq: resolvedSpaceSeq, fileUrl, fileName });
|
|
20
149
|
return textResult([
|
|
21
150
|
`Upload successful (external)`,
|
|
22
|
-
`Media ID: ${result.seq}`,
|
|
151
|
+
`Media ID (mediaSeq): ${result.seq}`,
|
|
23
152
|
`Duration: ${result.durationMs}ms`,
|
|
24
153
|
`Size: ${result.size} bytes`,
|
|
154
|
+
``,
|
|
155
|
+
`Use mediaSeq=${result.seq} when creating a translation via create_translation.`,
|
|
25
156
|
].join("\n"));
|
|
26
157
|
}
|
|
27
|
-
//
|
|
28
|
-
await client.post("/file/api/v1/media/validate", {
|
|
29
|
-
fileName,
|
|
30
|
-
fileUrl,
|
|
31
|
-
mediaType: mediaType.toUpperCase(),
|
|
32
|
-
});
|
|
158
|
+
// Blob upload — register the blob URL with PERSO
|
|
33
159
|
const endpoint = mediaType === "video"
|
|
34
160
|
? "/file/api/upload/video"
|
|
35
161
|
: "/file/api/upload/audio";
|
|
36
162
|
const result = await client.put(endpoint, {
|
|
37
|
-
spaceSeq,
|
|
163
|
+
spaceSeq: resolvedSpaceSeq,
|
|
38
164
|
fileUrl,
|
|
39
165
|
fileName,
|
|
40
166
|
});
|
|
41
167
|
const filePath = result.videoFilePath || result.audioFilePath || "";
|
|
42
168
|
return textResult([
|
|
43
169
|
`Upload successful`,
|
|
44
|
-
`Media ID: ${result.seq}`,
|
|
170
|
+
`Media ID (mediaSeq): ${result.seq}`,
|
|
45
171
|
`File Path: ${filePath}`,
|
|
46
172
|
`Duration: ${result.durationMs}ms`,
|
|
47
173
|
`Size: ${result.size} bytes`,
|
|
174
|
+
``,
|
|
175
|
+
`Use mediaSeq=${result.seq} when creating a translation via create_translation.`,
|
|
48
176
|
].join("\n"));
|
|
49
177
|
}
|
|
50
178
|
catch (error) {
|
|
51
179
|
return errorResult(error);
|
|
52
180
|
}
|
|
53
181
|
});
|
|
54
|
-
|
|
182
|
+
// ──────────────────────────────────────────────
|
|
183
|
+
// External Metadata — preview external media
|
|
184
|
+
// ──────────────────────────────────────────────
|
|
185
|
+
server.tool("get_external_metadata", [
|
|
186
|
+
"Preview metadata of an external video (YouTube, TikTok, Google Drive) without downloading.",
|
|
187
|
+
"Returns title, duration, and thumbnail. Use before validate_media and upload_media.",
|
|
188
|
+
"",
|
|
189
|
+
"** External Platform Upload Workflow **",
|
|
190
|
+
" Step 1: get_external_metadata ← YOU ARE HERE",
|
|
191
|
+
" Step 2: validate_media → check constraints",
|
|
192
|
+
" Step 3: upload_media (source='external') → start import",
|
|
193
|
+
].join("\n"), {
|
|
55
194
|
url: z.string().describe("YouTube, TikTok, or Google Drive URL"),
|
|
56
195
|
}, async ({ url }) => {
|
|
57
196
|
try {
|
package/dist/tools/lip-sync.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { PersoApiClient } from "../client.js";
|
|
3
|
-
|
|
3
|
+
import type { SpaceResolver } from "../utils/space-resolver.js";
|
|
4
|
+
export declare function registerLipSyncTools(server: McpServer, client: PersoApiClient, resolver: SpaceResolver): void;
|
package/dist/tools/lip-sync.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { errorResult, textResult } from "../utils/formatting.js";
|
|
3
|
-
export function registerLipSyncTools(server, client) {
|
|
3
|
+
export function registerLipSyncTools(server, client, resolver) {
|
|
4
4
|
// Tool: request_lip_sync
|
|
5
|
-
server.tool("request_lip_sync", "Request lip sync generation for a PERSO translation project. Creates a lip-synced version of the translated video.", {
|
|
5
|
+
server.tool("request_lip_sync", "Request lip sync generation for a PERSO translation project. Creates a lip-synced version of the translated video. spaceSeq is optional — omit to use the default space.", {
|
|
6
6
|
projectSeq: z.number().describe("Project ID"),
|
|
7
|
-
spaceSeq: z
|
|
7
|
+
spaceSeq: z
|
|
8
|
+
.number()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Space ID (omit to use default space)"),
|
|
8
11
|
preferredSpeedType: z
|
|
9
12
|
.enum(["GREEN", "RED"])
|
|
10
13
|
.optional()
|
|
@@ -12,7 +15,8 @@ export function registerLipSyncTools(server, client) {
|
|
|
12
15
|
.describe("GREEN = standard speed, RED = expedited"),
|
|
13
16
|
}, async ({ projectSeq, spaceSeq, preferredSpeedType }) => {
|
|
14
17
|
try {
|
|
15
|
-
const
|
|
18
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
19
|
+
const result = await client.post(`/video-translator/api/v1/projects/${projectSeq}/spaces/${resolvedSpaceSeq}/lip-sync`, { preferredSpeedType });
|
|
16
20
|
return textResult([
|
|
17
21
|
`Lip sync requested successfully.`,
|
|
18
22
|
`Generation IDs: ${result.generationIds?.join(", ") || "N/A"}`,
|
|
@@ -24,14 +28,18 @@ export function registerLipSyncTools(server, client) {
|
|
|
24
28
|
}
|
|
25
29
|
});
|
|
26
30
|
// Tool: get_lip_sync_history
|
|
27
|
-
server.tool("get_lip_sync_history", "Get the lip sync generation history for a PERSO project. Shows past VIEW and DOWNLOAD entries with status.", {
|
|
31
|
+
server.tool("get_lip_sync_history", "Get the lip sync generation history for a PERSO project. Shows past VIEW and DOWNLOAD entries with status. spaceSeq is optional — omit to use the default space.", {
|
|
28
32
|
projectSeq: z.number().describe("Project ID"),
|
|
29
|
-
spaceSeq: z
|
|
33
|
+
spaceSeq: z
|
|
34
|
+
.number()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Space ID (omit to use default space)"),
|
|
30
37
|
page: z.number().optional().default(0).describe("Page number"),
|
|
31
38
|
pageSize: z.number().optional().default(10).describe("Page size"),
|
|
32
39
|
}, async ({ projectSeq, spaceSeq, page, pageSize }) => {
|
|
33
40
|
try {
|
|
34
|
-
const
|
|
41
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
42
|
+
const result = await client.get(`/video-translator/api/v1/projects/${projectSeq}/spaces/${resolvedSpaceSeq}/lip-sync/generated`, { page: String(page), pageSize: String(pageSize) });
|
|
35
43
|
if (result.content.length === 0) {
|
|
36
44
|
return textResult("No lip sync history found.");
|
|
37
45
|
}
|
package/dist/tools/projects.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { PersoApiClient } from "../client.js";
|
|
3
|
-
|
|
3
|
+
import type { SpaceResolver } from "../utils/space-resolver.js";
|
|
4
|
+
export declare function registerProjectTools(server: McpServer, client: PersoApiClient, resolver: SpaceResolver): void;
|
package/dist/tools/projects.js
CHANGED
|
@@ -10,10 +10,13 @@ function formatProject(project) {
|
|
|
10
10
|
`Updated: ${project.updatedAt}`,
|
|
11
11
|
].join("\n");
|
|
12
12
|
}
|
|
13
|
-
export function registerProjectTools(server, client) {
|
|
13
|
+
export function registerProjectTools(server, client, resolver) {
|
|
14
14
|
// Tool: create_translation
|
|
15
|
-
server.tool("create_translation", "Create a new PERSO translation project. Upload media first using upload_media to get the mediaSeq. Returns project IDs for tracking.", {
|
|
16
|
-
spaceSeq: z
|
|
15
|
+
server.tool("create_translation", "Create a new PERSO translation project. Upload media first using upload_media to get the mediaSeq. Returns project IDs for tracking. spaceSeq is optional — omit to use the default space.", {
|
|
16
|
+
spaceSeq: z
|
|
17
|
+
.number()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Space ID (omit to use default space)"),
|
|
17
20
|
mediaSeq: z.number().describe("Media sequence ID from upload_media"),
|
|
18
21
|
isVideoProject: z
|
|
19
22
|
.boolean()
|
|
@@ -40,8 +43,9 @@ export function registerProjectTools(server, client) {
|
|
|
40
43
|
.describe("GREEN = standard speed, RED = expedited"),
|
|
41
44
|
}, async ({ spaceSeq, mediaSeq, isVideoProject, sourceLanguageCode, targetLanguageCodes, numberOfSpeakers, withLipSync, preferredSpeedType, }) => {
|
|
42
45
|
try {
|
|
46
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
43
47
|
// Initialize queue first (idempotent)
|
|
44
|
-
await client.put(`/video-translator/api/v1/projects/spaces/${
|
|
48
|
+
await client.put(`/video-translator/api/v1/projects/spaces/${resolvedSpaceSeq}/queue`);
|
|
45
49
|
const body = {
|
|
46
50
|
mediaSeq,
|
|
47
51
|
isVideoProject,
|
|
@@ -53,7 +57,7 @@ export function registerProjectTools(server, client) {
|
|
|
53
57
|
body.numberOfSpeakers = numberOfSpeakers;
|
|
54
58
|
if (withLipSync)
|
|
55
59
|
body.withLipSync = withLipSync;
|
|
56
|
-
const result = await client.post(`/video-translator/api/v1/projects/spaces/${
|
|
60
|
+
const result = await client.post(`/video-translator/api/v1/projects/spaces/${resolvedSpaceSeq}/translate`, body);
|
|
57
61
|
const ids = result.startGenerateProjectIdList;
|
|
58
62
|
return textResult([
|
|
59
63
|
`Translation project(s) created successfully!`,
|
|
@@ -66,8 +70,11 @@ export function registerProjectTools(server, client) {
|
|
|
66
70
|
}
|
|
67
71
|
});
|
|
68
72
|
// Tool: list_projects
|
|
69
|
-
server.tool("list_projects", "List PERSO translation projects in a space. Supports pagination, sorting, and filtering by type.", {
|
|
70
|
-
spaceSeq: z
|
|
73
|
+
server.tool("list_projects", "List PERSO translation projects in a space. Supports pagination, sorting, and filtering by type. spaceSeq is optional — omit to use the default space.", {
|
|
74
|
+
spaceSeq: z
|
|
75
|
+
.number()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Space ID (omit to use default space)"),
|
|
71
78
|
offset: z.number().optional().default(0).describe("Page offset"),
|
|
72
79
|
size: z.number().optional().default(20).describe("Page size (max 100)"),
|
|
73
80
|
sort: z
|
|
@@ -81,6 +88,7 @@ export function registerProjectTools(server, client) {
|
|
|
81
88
|
.describe("Filter by project type"),
|
|
82
89
|
}, async ({ spaceSeq, offset, size, sort, type }) => {
|
|
83
90
|
try {
|
|
91
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
84
92
|
const query = {
|
|
85
93
|
offset: String(offset),
|
|
86
94
|
size: String(size),
|
|
@@ -88,7 +96,7 @@ export function registerProjectTools(server, client) {
|
|
|
88
96
|
};
|
|
89
97
|
if (type)
|
|
90
98
|
query.type = type;
|
|
91
|
-
const result = await client.get(`/video-translator/api/v1/projects/spaces/${
|
|
99
|
+
const result = await client.get(`/video-translator/api/v1/projects/spaces/${resolvedSpaceSeq}`, query);
|
|
92
100
|
if (result.content.length === 0) {
|
|
93
101
|
return textResult("No projects found.");
|
|
94
102
|
}
|
|
@@ -101,9 +109,12 @@ export function registerProjectTools(server, client) {
|
|
|
101
109
|
}
|
|
102
110
|
});
|
|
103
111
|
// Tool: get_project
|
|
104
|
-
server.tool("get_project", "Get detailed information about a PERSO project. Use the 'include' parameter to fetch multiple types of info in one call.", {
|
|
112
|
+
server.tool("get_project", "Get detailed information about a PERSO project. Use the 'include' parameter to fetch multiple types of info in one call. spaceSeq is optional — omit to use the default space.", {
|
|
105
113
|
projectSeq: z.number().describe("Project ID"),
|
|
106
|
-
spaceSeq: z
|
|
114
|
+
spaceSeq: z
|
|
115
|
+
.number()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("Space ID (omit to use default space)"),
|
|
107
118
|
include: z
|
|
108
119
|
.array(z.enum([
|
|
109
120
|
"details",
|
|
@@ -118,8 +129,9 @@ export function registerProjectTools(server, client) {
|
|
|
118
129
|
.describe("What info to include: details, progress, video_info, download_info, used_features, retranslation_status"),
|
|
119
130
|
}, async ({ projectSeq, spaceSeq, include }) => {
|
|
120
131
|
try {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
132
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
133
|
+
const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${resolvedSpaceSeq}`;
|
|
134
|
+
const basePathAlt = `/video-translator/api/v1/projects/${projectSeq}/space/${resolvedSpaceSeq}`;
|
|
123
135
|
const sections = [];
|
|
124
136
|
const fetchers = {
|
|
125
137
|
details: async () => {
|
|
@@ -188,9 +200,12 @@ export function registerProjectTools(server, client) {
|
|
|
188
200
|
}
|
|
189
201
|
});
|
|
190
202
|
// Tool: manage_project
|
|
191
|
-
server.tool("manage_project", "Manage a PERSO project: update title, update access permissions, delete, or cancel translation.", {
|
|
203
|
+
server.tool("manage_project", "Manage a PERSO project: update title, update access permissions, delete, or cancel translation. spaceSeq is optional — omit to use the default space.", {
|
|
192
204
|
projectSeq: z.number().describe("Project ID"),
|
|
193
|
-
spaceSeq: z
|
|
205
|
+
spaceSeq: z
|
|
206
|
+
.number()
|
|
207
|
+
.optional()
|
|
208
|
+
.describe("Space ID (omit to use default space)"),
|
|
194
209
|
action: z
|
|
195
210
|
.enum(["update_title", "update_access", "delete", "cancel"])
|
|
196
211
|
.describe("Action to perform"),
|
|
@@ -204,7 +219,8 @@ export function registerProjectTools(server, client) {
|
|
|
204
219
|
.describe("Access level (required for update_access)"),
|
|
205
220
|
}, async ({ projectSeq, spaceSeq, action, title, accessPermission }) => {
|
|
206
221
|
try {
|
|
207
|
-
const
|
|
222
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
223
|
+
const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${resolvedSpaceSeq}`;
|
|
208
224
|
switch (action) {
|
|
209
225
|
case "update_title": {
|
|
210
226
|
if (!title) {
|
|
@@ -237,9 +253,12 @@ export function registerProjectTools(server, client) {
|
|
|
237
253
|
}
|
|
238
254
|
});
|
|
239
255
|
// Tool: download_project
|
|
240
|
-
server.tool("download_project", "Get download links for a PERSO project's translated files (video, audio, SRT) or view export history.", {
|
|
256
|
+
server.tool("download_project", "Get download links for a PERSO project's translated files (video, audio, SRT) or view export history. spaceSeq is optional — omit to use the default space.", {
|
|
241
257
|
projectSeq: z.number().describe("Project ID"),
|
|
242
|
-
spaceSeq: z
|
|
258
|
+
spaceSeq: z
|
|
259
|
+
.number()
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("Space ID (omit to use default space)"),
|
|
243
262
|
action: z
|
|
244
263
|
.enum(["get_links", "export_history"])
|
|
245
264
|
.optional()
|
|
@@ -247,8 +266,9 @@ export function registerProjectTools(server, client) {
|
|
|
247
266
|
.describe("get_links: download URLs. export_history: past export records."),
|
|
248
267
|
}, async ({ projectSeq, spaceSeq, action }) => {
|
|
249
268
|
try {
|
|
269
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
250
270
|
if (action === "export_history") {
|
|
251
|
-
const basePath = `/video-translator/api/v1/projects/${projectSeq}/space/${
|
|
271
|
+
const basePath = `/video-translator/api/v1/projects/${projectSeq}/space/${resolvedSpaceSeq}`;
|
|
252
272
|
const history = await client.get(`${basePath}/export-history`);
|
|
253
273
|
if (history.content.length === 0) {
|
|
254
274
|
return textResult("No export history found.");
|
|
@@ -257,7 +277,7 @@ export function registerProjectTools(server, client) {
|
|
|
257
277
|
return textResult(`Export History:\n${entries.join("\n")}`);
|
|
258
278
|
}
|
|
259
279
|
// get_links
|
|
260
|
-
const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${
|
|
280
|
+
const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${resolvedSpaceSeq}`;
|
|
261
281
|
const links = await client.get(`${basePath}/download`);
|
|
262
282
|
const parts = ["Download Links:"];
|
|
263
283
|
if (links.video)
|
package/dist/tools/scripts.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { PersoApiClient } from "../client.js";
|
|
3
|
-
|
|
3
|
+
import type { SpaceResolver } from "../utils/space-resolver.js";
|
|
4
|
+
export declare function registerScriptTools(server: McpServer, client: PersoApiClient, resolver: SpaceResolver): void;
|
package/dist/tools/scripts.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { errorResult, textResult } from "../utils/formatting.js";
|
|
3
|
-
export function registerScriptTools(server, client) {
|
|
3
|
+
export function registerScriptTools(server, client, resolver) {
|
|
4
4
|
// Tool: get_script
|
|
5
|
-
server.tool("get_script", "Get the translation script for a PERSO project. Returns sentences with original text, translated text, matching rate, and audio URLs. Supports cursor-based pagination.", {
|
|
5
|
+
server.tool("get_script", "Get the translation script for a PERSO project. Returns sentences with original text, translated text, matching rate, and audio URLs. Supports cursor-based pagination. spaceSeq is optional — omit to use the default space.", {
|
|
6
6
|
projectSeq: z.number().describe("Project ID"),
|
|
7
|
-
spaceSeq: z
|
|
7
|
+
spaceSeq: z
|
|
8
|
+
.number()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Space ID (omit to use default space)"),
|
|
8
11
|
cursorId: z
|
|
9
12
|
.number()
|
|
10
13
|
.optional()
|
|
@@ -16,7 +19,8 @@ export function registerScriptTools(server, client) {
|
|
|
16
19
|
.describe("Number of sentences to return"),
|
|
17
20
|
}, async ({ projectSeq, spaceSeq, cursorId, size }) => {
|
|
18
21
|
try {
|
|
19
|
-
const
|
|
22
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
23
|
+
const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${resolvedSpaceSeq}/script`;
|
|
20
24
|
const query = { size: String(size) };
|
|
21
25
|
if (cursorId !== undefined)
|
|
22
26
|
query.cursorId = String(cursorId);
|
|
@@ -123,9 +127,12 @@ export function registerScriptTools(server, client) {
|
|
|
123
127
|
}
|
|
124
128
|
});
|
|
125
129
|
// Tool: request_proofread
|
|
126
|
-
server.tool("request_proofread", "Request AI proofreading for all sentences in a PERSO translation project.", {
|
|
130
|
+
server.tool("request_proofread", "Request AI proofreading for all sentences in a PERSO translation project. spaceSeq is optional — omit to use the default space.", {
|
|
127
131
|
projectSeq: z.number().describe("Project ID"),
|
|
128
|
-
spaceSeq: z
|
|
132
|
+
spaceSeq: z
|
|
133
|
+
.number()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("Space ID (omit to use default space)"),
|
|
129
136
|
isLipSync: z
|
|
130
137
|
.boolean()
|
|
131
138
|
.optional()
|
|
@@ -138,7 +145,8 @@ export function registerScriptTools(server, client) {
|
|
|
138
145
|
.describe("GREEN = standard, RED = expedited"),
|
|
139
146
|
}, async ({ projectSeq, spaceSeq, isLipSync, preferredSpeedType }) => {
|
|
140
147
|
try {
|
|
141
|
-
await
|
|
148
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
149
|
+
await client.post(`/video-translator/api/v1/project/${projectSeq}/space/${resolvedSpaceSeq}/proofread`, { isLipSync, preferredSpeedType });
|
|
142
150
|
return textResult("Proofread request submitted successfully.");
|
|
143
151
|
}
|
|
144
152
|
catch (error) {
|
package/dist/tools/spaces.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { PersoApiClient } from "../client.js";
|
|
3
|
-
|
|
3
|
+
import type { SpaceResolver } from "../utils/space-resolver.js";
|
|
4
|
+
export declare function registerSpaceTools(server: McpServer, client: PersoApiClient, resolver: SpaceResolver): void;
|
package/dist/tools/spaces.js
CHANGED
|
@@ -8,7 +8,7 @@ function formatSpace(space) {
|
|
|
8
8
|
`Role: ${space.memberRole}`,
|
|
9
9
|
].join("\n");
|
|
10
10
|
}
|
|
11
|
-
export function registerSpaceTools(server, client) {
|
|
11
|
+
export function registerSpaceTools(server, client, resolver) {
|
|
12
12
|
server.tool("list_spaces", "List all PERSO spaces the user belongs to, or get a specific space by ID. Returns space name, plan, member count, and role.", {
|
|
13
13
|
spaceSeq: z
|
|
14
14
|
.number()
|
|
@@ -30,4 +30,22 @@ export function registerSpaceTools(server, client) {
|
|
|
30
30
|
return errorResult(error);
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
|
+
server.tool("set_default_space", "Set the default space for all subsequent API calls. Once set, you can omit spaceSeq in other tools. If not called, the first space is used automatically.", {
|
|
34
|
+
spaceSeq: z.number().describe("Space ID to set as default"),
|
|
35
|
+
}, async ({ spaceSeq }) => {
|
|
36
|
+
try {
|
|
37
|
+
// Verify the space exists
|
|
38
|
+
const space = await client.get(`/portal/api/v1/spaces/${spaceSeq}`);
|
|
39
|
+
resolver.setDefault(spaceSeq);
|
|
40
|
+
return textResult([
|
|
41
|
+
`Default space set successfully.`,
|
|
42
|
+
formatSpace(space),
|
|
43
|
+
``,
|
|
44
|
+
`All tools will now use this space when spaceSeq is omitted.`,
|
|
45
|
+
].join("\n"));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return errorResult(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
33
51
|
}
|
package/dist/tools/usage.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { PersoApiClient } from "../client.js";
|
|
3
|
-
|
|
3
|
+
import type { SpaceResolver } from "../utils/space-resolver.js";
|
|
4
|
+
export declare function registerUsageTools(server: McpServer, client: PersoApiClient, resolver: SpaceResolver): void;
|
package/dist/tools/usage.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { errorResult, textResult } from "../utils/formatting.js";
|
|
3
|
-
export function registerUsageTools(server, client) {
|
|
4
|
-
server.tool("get_usage", "Get PERSO plan status, estimate quota usage for a media file, or check queue status.", {
|
|
5
|
-
spaceSeq: z
|
|
3
|
+
export function registerUsageTools(server, client, resolver) {
|
|
4
|
+
server.tool("get_usage", "Get PERSO plan status, estimate quota usage for a media file, or check queue status. spaceSeq is optional — omit to use the default space.", {
|
|
5
|
+
spaceSeq: z
|
|
6
|
+
.number()
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("Space ID (omit to use default space)"),
|
|
6
9
|
action: z
|
|
7
10
|
.enum(["plan_status", "estimate_quota", "queue_status"])
|
|
8
11
|
.default("plan_status")
|
|
@@ -27,7 +30,8 @@ export function registerUsageTools(server, client) {
|
|
|
27
30
|
.describe("Number of target languages (for estimate_quota)"),
|
|
28
31
|
}, async ({ spaceSeq, action, mediaType, durationMs, lipSync, width, height, targetLanguageSize }) => {
|
|
29
32
|
try {
|
|
30
|
-
const
|
|
33
|
+
const resolvedSpaceSeq = await resolver.resolve(spaceSeq);
|
|
34
|
+
const basePath = `/video-translator/api/v1/projects/spaces/${resolvedSpaceSeq}`;
|
|
31
35
|
if (action === "plan_status") {
|
|
32
36
|
const status = await client.get(`${basePath}/plan/status`);
|
|
33
37
|
return textResult([
|
package/dist/types.d.ts
CHANGED
|
@@ -18,6 +18,17 @@ export interface ExternalMetadata {
|
|
|
18
18
|
durationMs: number;
|
|
19
19
|
thumbnailUrl: string;
|
|
20
20
|
}
|
|
21
|
+
export interface SasTokenResult {
|
|
22
|
+
blobSasUrl: string;
|
|
23
|
+
expiresOn: string;
|
|
24
|
+
}
|
|
25
|
+
export interface MediaValidationResult {
|
|
26
|
+
valid: boolean;
|
|
27
|
+
maxDurationMs?: number;
|
|
28
|
+
maxSizeBytes?: number;
|
|
29
|
+
maxResolution?: string;
|
|
30
|
+
message?: string;
|
|
31
|
+
}
|
|
21
32
|
export interface Project {
|
|
22
33
|
projectSeq: number;
|
|
23
34
|
title: string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PersoApiClient } from "../client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Caches the default spaceSeq in memory so tools don't need to
|
|
4
|
+
* repeatedly call the Spaces API.
|
|
5
|
+
*
|
|
6
|
+
* - resolve(spaceSeq?) → returns the provided value, or the cached default.
|
|
7
|
+
* - On first call without a value, fetches spaces from the API and caches the first one.
|
|
8
|
+
* - setDefault(spaceSeq) allows explicit override (e.g., via set_default_space tool).
|
|
9
|
+
*/
|
|
10
|
+
export declare class SpaceResolver {
|
|
11
|
+
private defaultSpaceSeq;
|
|
12
|
+
private client;
|
|
13
|
+
constructor(client: PersoApiClient);
|
|
14
|
+
/** Resolve spaceSeq: use the given value, the cached default, or auto-fetch. */
|
|
15
|
+
resolve(spaceSeq?: number): Promise<number>;
|
|
16
|
+
/** Explicitly set the default spaceSeq. */
|
|
17
|
+
setDefault(spaceSeq: number): void;
|
|
18
|
+
/** Get the currently cached default (null if not yet resolved). */
|
|
19
|
+
getDefault(): number | null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caches the default spaceSeq in memory so tools don't need to
|
|
3
|
+
* repeatedly call the Spaces API.
|
|
4
|
+
*
|
|
5
|
+
* - resolve(spaceSeq?) → returns the provided value, or the cached default.
|
|
6
|
+
* - On first call without a value, fetches spaces from the API and caches the first one.
|
|
7
|
+
* - setDefault(spaceSeq) allows explicit override (e.g., via set_default_space tool).
|
|
8
|
+
*/
|
|
9
|
+
export class SpaceResolver {
|
|
10
|
+
defaultSpaceSeq = null;
|
|
11
|
+
client;
|
|
12
|
+
constructor(client) {
|
|
13
|
+
this.client = client;
|
|
14
|
+
}
|
|
15
|
+
/** Resolve spaceSeq: use the given value, the cached default, or auto-fetch. */
|
|
16
|
+
async resolve(spaceSeq) {
|
|
17
|
+
if (spaceSeq !== undefined) {
|
|
18
|
+
return spaceSeq;
|
|
19
|
+
}
|
|
20
|
+
if (this.defaultSpaceSeq !== null) {
|
|
21
|
+
return this.defaultSpaceSeq;
|
|
22
|
+
}
|
|
23
|
+
const spaces = await this.client.get("/portal/api/v1/spaces");
|
|
24
|
+
if (spaces.length === 0) {
|
|
25
|
+
throw new Error("No spaces found. Create a space on PERSO first, or provide spaceSeq explicitly.");
|
|
26
|
+
}
|
|
27
|
+
this.defaultSpaceSeq = spaces[0].spaceSeq;
|
|
28
|
+
return this.defaultSpaceSeq;
|
|
29
|
+
}
|
|
30
|
+
/** Explicitly set the default spaceSeq. */
|
|
31
|
+
setDefault(spaceSeq) {
|
|
32
|
+
this.defaultSpaceSeq = spaceSeq;
|
|
33
|
+
}
|
|
34
|
+
/** Get the currently cached default (null if not yet resolved). */
|
|
35
|
+
getDefault() {
|
|
36
|
+
return this.defaultSpaceSeq;
|
|
37
|
+
}
|
|
38
|
+
}
|