perso-mcp-server 1.0.0 → 1.0.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.
- package/dist/client.d.ts +5 -0
- package/dist/client.js +24 -0
- package/dist/tools/files.d.ts +1 -1
- package/dist/tools/files.js +148 -14
- package/dist/types.d.ts +11 -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/tools/files.d.ts
CHANGED
package/dist/tools/files.js
CHANGED
|
@@ -1,15 +1,139 @@
|
|
|
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
5
|
export function registerFileTools(server, client) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
"",
|
|
122
|
+
"source='blob' — Use after uploading a local file via get_sas_token + upload_file_to_blob.",
|
|
123
|
+
" Provide the blob URL (returned by upload_file_to_blob) as fileUrl.",
|
|
124
|
+
"source='external' — Use for YouTube / TikTok / Google Drive URLs.",
|
|
125
|
+
" Optionally call get_external_metadata and validate_media first.",
|
|
126
|
+
].join("\n"), {
|
|
127
|
+
spaceSeq: z.number().describe("Space ID to upload to"),
|
|
128
|
+
source: z
|
|
129
|
+
.enum(["blob", "external"])
|
|
130
|
+
.describe("'blob' for files uploaded via SAS URL. 'external' for YouTube/TikTok/Google Drive URL."),
|
|
131
|
+
mediaType: z
|
|
132
|
+
.enum(["video", "audio"])
|
|
133
|
+
.describe("Type of media to register"),
|
|
134
|
+
fileUrl: z
|
|
135
|
+
.string()
|
|
136
|
+
.describe("Blob URL (from upload_file_to_blob) or external platform URL"),
|
|
13
137
|
fileName: z
|
|
14
138
|
.string()
|
|
15
139
|
.describe("File name with extension (e.g., 'my_video.mp4')"),
|
|
@@ -19,17 +143,14 @@ export function registerFileTools(server, client) {
|
|
|
19
143
|
const result = await client.put("/file/api/upload/video/external", { spaceSeq, fileUrl, fileName });
|
|
20
144
|
return textResult([
|
|
21
145
|
`Upload successful (external)`,
|
|
22
|
-
`Media ID: ${result.seq}`,
|
|
146
|
+
`Media ID (mediaSeq): ${result.seq}`,
|
|
23
147
|
`Duration: ${result.durationMs}ms`,
|
|
24
148
|
`Size: ${result.size} bytes`,
|
|
149
|
+
``,
|
|
150
|
+
`Use mediaSeq=${result.seq} when creating a translation via create_translation.`,
|
|
25
151
|
].join("\n"));
|
|
26
152
|
}
|
|
27
|
-
//
|
|
28
|
-
await client.post("/file/api/v1/media/validate", {
|
|
29
|
-
fileName,
|
|
30
|
-
fileUrl,
|
|
31
|
-
mediaType: mediaType.toUpperCase(),
|
|
32
|
-
});
|
|
153
|
+
// Blob upload — register the blob URL with PERSO
|
|
33
154
|
const endpoint = mediaType === "video"
|
|
34
155
|
? "/file/api/upload/video"
|
|
35
156
|
: "/file/api/upload/audio";
|
|
@@ -41,17 +162,30 @@ export function registerFileTools(server, client) {
|
|
|
41
162
|
const filePath = result.videoFilePath || result.audioFilePath || "";
|
|
42
163
|
return textResult([
|
|
43
164
|
`Upload successful`,
|
|
44
|
-
`Media ID: ${result.seq}`,
|
|
165
|
+
`Media ID (mediaSeq): ${result.seq}`,
|
|
45
166
|
`File Path: ${filePath}`,
|
|
46
167
|
`Duration: ${result.durationMs}ms`,
|
|
47
168
|
`Size: ${result.size} bytes`,
|
|
169
|
+
``,
|
|
170
|
+
`Use mediaSeq=${result.seq} when creating a translation via create_translation.`,
|
|
48
171
|
].join("\n"));
|
|
49
172
|
}
|
|
50
173
|
catch (error) {
|
|
51
174
|
return errorResult(error);
|
|
52
175
|
}
|
|
53
176
|
});
|
|
54
|
-
|
|
177
|
+
// ──────────────────────────────────────────────
|
|
178
|
+
// External Metadata — preview external media
|
|
179
|
+
// ──────────────────────────────────────────────
|
|
180
|
+
server.tool("get_external_metadata", [
|
|
181
|
+
"Preview metadata of an external video (YouTube, TikTok, Google Drive) without downloading.",
|
|
182
|
+
"Returns title, duration, and thumbnail. Use before validate_media and upload_media.",
|
|
183
|
+
"",
|
|
184
|
+
"** External Platform Upload Workflow **",
|
|
185
|
+
" Step 1: get_external_metadata ← YOU ARE HERE",
|
|
186
|
+
" Step 2: validate_media → check constraints",
|
|
187
|
+
" Step 3: upload_media (source='external') → start import",
|
|
188
|
+
].join("\n"), {
|
|
55
189
|
url: z.string().describe("YouTube, TikTok, or Google Drive URL"),
|
|
56
190
|
}, async ({ url }) => {
|
|
57
191
|
try {
|
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;
|