perso-mcp-server 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.
@@ -0,0 +1,19 @@
1
+ export declare class PersoApiError extends Error {
2
+ readonly code: string;
3
+ readonly status: number;
4
+ readonly description: string;
5
+ constructor(code: string, status: number, description: string);
6
+ }
7
+ export declare class PersoApiClient {
8
+ private baseUrl;
9
+ private apiKey;
10
+ constructor(baseUrl: string, apiKey: string);
11
+ private buildUrl;
12
+ request<T>(method: string, path: string, body?: unknown, query?: Record<string, string>): Promise<T>;
13
+ get<T>(path: string, query?: Record<string, string>): Promise<T>;
14
+ post<T>(path: string, body?: unknown, query?: Record<string, string>): Promise<T>;
15
+ put<T>(path: string, body?: unknown, query?: Record<string, string>): Promise<T>;
16
+ patch<T>(path: string, body?: unknown, query?: Record<string, string>): Promise<T>;
17
+ delete(path: string, query?: Record<string, string>): Promise<void>;
18
+ }
19
+ export declare function createClient(): PersoApiClient;
package/dist/client.js ADDED
@@ -0,0 +1,93 @@
1
+ export class PersoApiError extends Error {
2
+ code;
3
+ status;
4
+ description;
5
+ constructor(code, status, description) {
6
+ super(`[${code}] ${description}`);
7
+ this.code = code;
8
+ this.status = status;
9
+ this.description = description;
10
+ this.name = "PersoApiError";
11
+ }
12
+ }
13
+ export class PersoApiClient {
14
+ baseUrl;
15
+ apiKey;
16
+ constructor(baseUrl, apiKey) {
17
+ this.baseUrl = baseUrl.replace(/\/$/, "");
18
+ this.apiKey = apiKey;
19
+ }
20
+ buildUrl(path, query) {
21
+ const url = new URL(path, this.baseUrl);
22
+ if (query) {
23
+ for (const [key, value] of Object.entries(query)) {
24
+ if (value !== undefined && value !== null) {
25
+ url.searchParams.set(key, value);
26
+ }
27
+ }
28
+ }
29
+ return url.toString();
30
+ }
31
+ async request(method, path, body, query) {
32
+ const url = this.buildUrl(path, query);
33
+ const headers = {
34
+ "XP-API-KEY": this.apiKey,
35
+ "Content-Type": "application/json",
36
+ };
37
+ const response = await fetch(url, {
38
+ method,
39
+ headers,
40
+ body: body ? JSON.stringify(body) : undefined,
41
+ });
42
+ if (!response.ok) {
43
+ let errorCode = "UNKNOWN";
44
+ let errorDescription = `HTTP ${response.status}`;
45
+ try {
46
+ const errorBody = await response.json();
47
+ if (errorBody.code)
48
+ errorCode = errorBody.code;
49
+ if (errorBody.message)
50
+ errorDescription = errorBody.message;
51
+ if (errorBody.description)
52
+ errorDescription = errorBody.description;
53
+ }
54
+ catch {
55
+ // response body is not JSON
56
+ }
57
+ throw new PersoApiError(errorCode, response.status, errorDescription);
58
+ }
59
+ if (response.status === 204) {
60
+ return undefined;
61
+ }
62
+ const json = await response.json();
63
+ // Unwrap {"result": {...}} if present
64
+ if (json && typeof json === "object" && "result" in json) {
65
+ return json.result;
66
+ }
67
+ return json;
68
+ }
69
+ async get(path, query) {
70
+ return this.request("GET", path, undefined, query);
71
+ }
72
+ async post(path, body, query) {
73
+ return this.request("POST", path, body, query);
74
+ }
75
+ async put(path, body, query) {
76
+ return this.request("PUT", path, body, query);
77
+ }
78
+ async patch(path, body, query) {
79
+ return this.request("PATCH", path, body, query);
80
+ }
81
+ async delete(path, query) {
82
+ return this.request("DELETE", path, undefined, query);
83
+ }
84
+ }
85
+ export function createClient() {
86
+ const apiKey = process.env.PERSO_API_KEY;
87
+ if (!apiKey) {
88
+ throw new Error("PERSO_API_KEY environment variable is required. " +
89
+ "Set it to your PERSO API key (e.g., pk_live_...).");
90
+ }
91
+ const baseUrl = process.env.PERSO_API_BASE_URL || "https://dev-developers.perso.ai";
92
+ return new PersoApiClient(baseUrl, apiKey);
93
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { createClient } from "./client.js";
5
+ import { registerSpaceTools } from "./tools/spaces.js";
6
+ import { registerFileTools } from "./tools/files.js";
7
+ import { registerProjectTools } from "./tools/projects.js";
8
+ import { registerScriptTools } from "./tools/scripts.js";
9
+ import { registerUsageTools } from "./tools/usage.js";
10
+ import { registerLanguageTools } from "./tools/languages.js";
11
+ import { registerCommunityTools } from "./tools/community.js";
12
+ import { registerLipSyncTools } from "./tools/lip-sync.js";
13
+ import { registerFeedbackTools } from "./tools/feedback.js";
14
+ async function main() {
15
+ const client = createClient();
16
+ const server = new McpServer({
17
+ name: "perso-mcp-server",
18
+ version: "1.0.0",
19
+ });
20
+ registerSpaceTools(server, client);
21
+ registerFileTools(server, client);
22
+ registerProjectTools(server, client);
23
+ registerScriptTools(server, client);
24
+ registerUsageTools(server, client);
25
+ registerLanguageTools(server, client);
26
+ registerCommunityTools(server, client);
27
+ registerLipSyncTools(server, client);
28
+ registerFeedbackTools(server, client);
29
+ const transport = new StdioServerTransport();
30
+ await server.connect(transport);
31
+ console.error("PERSO MCP Server running on stdio");
32
+ }
33
+ main().catch((error) => {
34
+ console.error("Fatal error:", error);
35
+ process.exit(1);
36
+ });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerCommunityTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ import { errorResult, textResult } from "../utils/formatting.js";
3
+ export function registerCommunityTools(server, client) {
4
+ // Tool: manage_sharing
5
+ server.tool("manage_sharing", "Get or toggle the share link for a PERSO project.", {
6
+ projectSeq: z.number().describe("Project ID"),
7
+ action: z
8
+ .enum(["get_link", "toggle_sharing"])
9
+ .describe("get_link: get share URL. toggle_sharing: enable/disable sharing."),
10
+ enabled: z
11
+ .boolean()
12
+ .optional()
13
+ .describe("Enable or disable sharing (required for toggle_sharing)"),
14
+ }, async ({ projectSeq, action, enabled }) => {
15
+ try {
16
+ const basePath = `/video-translator/api/v1/projects/${projectSeq}/share`;
17
+ if (action === "get_link") {
18
+ const result = await client.get(basePath);
19
+ return textResult(`Share link query: ${result.shareQuery}`);
20
+ }
21
+ // toggle_sharing
22
+ if (enabled === undefined) {
23
+ return errorResult(new Error("enabled is required for toggle_sharing"));
24
+ }
25
+ await client.patch(basePath, undefined, {
26
+ sharedStatus: String(enabled),
27
+ });
28
+ return textResult(`Sharing ${enabled ? "enabled" : "disabled"} for project ${projectSeq}.`);
29
+ }
30
+ catch (error) {
31
+ return errorResult(error);
32
+ }
33
+ });
34
+ // Tool: browse_community
35
+ server.tool("browse_community", "Browse PERSO community featured projects or view a shared project.", {
36
+ action: z
37
+ .enum(["list_featured", "get_featured", "get_shared"])
38
+ .describe("list_featured: browse featured projects. get_featured: get details of a featured project. get_shared: view a shared project by query string."),
39
+ projectSeq: z
40
+ .number()
41
+ .optional()
42
+ .describe("Project ID (required for get_featured)"),
43
+ sharedQuery: z
44
+ .string()
45
+ .optional()
46
+ .describe("Encrypted query string (required for get_shared)"),
47
+ page: z.number().optional().default(0).describe("Page number"),
48
+ size: z.number().optional().default(10).describe("Page size"),
49
+ languageCode: z
50
+ .string()
51
+ .optional()
52
+ .describe("Filter by language code (for list_featured)"),
53
+ }, async ({ action, projectSeq, sharedQuery, page, size, languageCode }) => {
54
+ try {
55
+ const basePath = "/video-translator/api/v1/projects";
56
+ if (action === "list_featured") {
57
+ const query = {
58
+ page: String(page),
59
+ size: String(size),
60
+ };
61
+ if (languageCode)
62
+ query.languageCode = languageCode;
63
+ const result = await client.get(`${basePath}/recommended`, query);
64
+ if (result.content.length === 0) {
65
+ return textResult("No featured projects found.");
66
+ }
67
+ const lines = result.content.map((p) => `${p.title} (ID: ${p.projectSeq}) - ${p.sourceLanguageCode} → ${p.targetLanguageCode}`);
68
+ return textResult(`Featured Projects:\n${lines.join("\n")}`);
69
+ }
70
+ if (action === "get_featured") {
71
+ if (projectSeq === undefined) {
72
+ return errorResult(new Error("projectSeq is required for get_featured"));
73
+ }
74
+ const project = await client.get(`${basePath}/recommended/${projectSeq}`);
75
+ return textResult([
76
+ `Title: ${project.title}`,
77
+ `Languages: ${project.sourceLanguageCode} → ${project.targetLanguageCode}`,
78
+ project.videoUrl ? `Video: ${project.videoUrl}` : null,
79
+ `Thumbnail: ${project.thumbnailUrl}`,
80
+ ]
81
+ .filter(Boolean)
82
+ .join("\n"));
83
+ }
84
+ // get_shared
85
+ if (!sharedQuery) {
86
+ return errorResult(new Error("sharedQuery is required for get_shared"));
87
+ }
88
+ const shared = await client.get(`${basePath}/shared/${sharedQuery}`);
89
+ return textResult([
90
+ `Shared Project: ${shared.title}`,
91
+ `Languages: ${shared.sourceLanguageCode} → ${shared.targetLanguageCode}`,
92
+ shared.videoUrl ? `Video: ${shared.videoUrl}` : null,
93
+ ]
94
+ .filter(Boolean)
95
+ .join("\n"));
96
+ }
97
+ catch (error) {
98
+ return errorResult(error);
99
+ }
100
+ });
101
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerFeedbackTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import { errorResult, textResult } from "../utils/formatting.js";
3
+ export function registerFeedbackTools(server, client) {
4
+ server.tool("manage_feedback", "Submit or retrieve feedback for a PERSO translation project. Rate translations from 1 to 5.", {
5
+ action: z
6
+ .enum(["submit", "get"])
7
+ .describe("submit: rate a project. get: retrieve existing feedback."),
8
+ projectSeq: z.number().describe("Project ID"),
9
+ rating: z
10
+ .number()
11
+ .min(1)
12
+ .max(5)
13
+ .optional()
14
+ .describe("Rating from 1 to 5 (required for submit)"),
15
+ }, async ({ action, projectSeq, rating }) => {
16
+ try {
17
+ const basePath = "/video-translator/api/v1/projects/feedbacks";
18
+ if (action === "submit") {
19
+ if (rating === undefined) {
20
+ return errorResult(new Error("rating is required for submit"));
21
+ }
22
+ const result = await client.post(basePath, {
23
+ projectSeq,
24
+ rating,
25
+ });
26
+ return textResult([
27
+ `Feedback submitted.`,
28
+ `Average Rating: ${result.averageRating}`,
29
+ `Total Feedbacks: ${result.count}`,
30
+ ].join("\n"));
31
+ }
32
+ // get
33
+ const result = await client.get(basePath, {
34
+ projectSeq: String(projectSeq),
35
+ });
36
+ if (!result) {
37
+ return textResult("No feedback submitted for this project yet.");
38
+ }
39
+ return textResult([
40
+ `Project ${projectSeq} Feedback:`,
41
+ `Your Rating: ${result.rating}`,
42
+ `Average Rating: ${result.averageRating}`,
43
+ `Total Feedbacks: ${result.count}`,
44
+ ].join("\n"));
45
+ }
46
+ catch (error) {
47
+ return errorResult(error);
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerFileTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { errorResult, textResult } from "../utils/formatting.js";
3
+ export function registerFileTools(server, client) {
4
+ server.tool("upload_media", "Upload a video or audio file to PERSO. Supports direct file URLs and external platform URLs (YouTube, TikTok, Google Drive). Returns mediaSeq needed for creating translations.", {
5
+ spaceSeq: z.number().describe("Space ID to upload to"),
6
+ source: z
7
+ .enum(["url", "external"])
8
+ .describe("'url' for direct file URL (.mp4, .webm, .mov, .mp3, .wav). 'external' for YouTube/TikTok/Google Drive URL."),
9
+ mediaType: z
10
+ .enum(["video", "audio"])
11
+ .describe("Type of media to upload"),
12
+ fileUrl: z.string().describe("Direct file URL or external platform URL"),
13
+ fileName: z
14
+ .string()
15
+ .describe("File name with extension (e.g., 'my_video.mp4')"),
16
+ }, async ({ spaceSeq, source, mediaType, fileUrl, fileName }) => {
17
+ try {
18
+ if (source === "external") {
19
+ const result = await client.put("/file/api/upload/video/external", { spaceSeq, fileUrl, fileName });
20
+ return textResult([
21
+ `Upload successful (external)`,
22
+ `Media ID: ${result.seq}`,
23
+ `Duration: ${result.durationMs}ms`,
24
+ `Size: ${result.size} bytes`,
25
+ ].join("\n"));
26
+ }
27
+ // Direct URL upload - validate first, then upload
28
+ await client.post("/file/api/v1/media/validate", {
29
+ fileName,
30
+ fileUrl,
31
+ mediaType: mediaType.toUpperCase(),
32
+ });
33
+ const endpoint = mediaType === "video"
34
+ ? "/file/api/upload/video"
35
+ : "/file/api/upload/audio";
36
+ const result = await client.put(endpoint, {
37
+ spaceSeq,
38
+ fileUrl,
39
+ fileName,
40
+ });
41
+ const filePath = result.videoFilePath || result.audioFilePath || "";
42
+ return textResult([
43
+ `Upload successful`,
44
+ `Media ID: ${result.seq}`,
45
+ `File Path: ${filePath}`,
46
+ `Duration: ${result.durationMs}ms`,
47
+ `Size: ${result.size} bytes`,
48
+ ].join("\n"));
49
+ }
50
+ catch (error) {
51
+ return errorResult(error);
52
+ }
53
+ });
54
+ server.tool("get_external_metadata", "Preview metadata of an external video (YouTube, TikTok, Google Drive) without downloading. Returns title, duration, and thumbnail.", {
55
+ url: z.string().describe("YouTube, TikTok, or Google Drive URL"),
56
+ }, async ({ url }) => {
57
+ try {
58
+ const metadata = await client.post("/file/api/v1/video-translator/external/metadata", { url });
59
+ return textResult([
60
+ `Title: ${metadata.title}`,
61
+ `Duration: ${metadata.durationMs}ms`,
62
+ `Thumbnail: ${metadata.thumbnailUrl}`,
63
+ ].join("\n"));
64
+ }
65
+ catch (error) {
66
+ return errorResult(error);
67
+ }
68
+ });
69
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerLanguageTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,13 @@
1
+ import { errorResult, textResult } from "../utils/formatting.js";
2
+ export function registerLanguageTools(server, client) {
3
+ server.tool("list_languages", "List all supported languages for PERSO video translation. Returns language codes, names, and native names.", {}, async () => {
4
+ try {
5
+ const languages = await client.get("/video-translator/api/v1/languages");
6
+ const lines = languages.map((lang) => `${lang.languageCode}: ${lang.languageName} (${lang.nativeName})`);
7
+ return textResult(`Supported Languages (${languages.length}):\n${lines.join("\n")}`);
8
+ }
9
+ catch (error) {
10
+ return errorResult(error);
11
+ }
12
+ });
13
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerLipSyncTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import { errorResult, textResult } from "../utils/formatting.js";
3
+ export function registerLipSyncTools(server, client) {
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.", {
6
+ projectSeq: z.number().describe("Project ID"),
7
+ spaceSeq: z.number().describe("Space ID"),
8
+ preferredSpeedType: z
9
+ .enum(["GREEN", "RED"])
10
+ .optional()
11
+ .default("GREEN")
12
+ .describe("GREEN = standard speed, RED = expedited"),
13
+ }, async ({ projectSeq, spaceSeq, preferredSpeedType }) => {
14
+ try {
15
+ const result = await client.post(`/video-translator/api/v1/projects/${projectSeq}/spaces/${spaceSeq}/lip-sync`, { preferredSpeedType });
16
+ return textResult([
17
+ `Lip sync requested successfully.`,
18
+ `Generation IDs: ${result.generationIds?.join(", ") || "N/A"}`,
19
+ `Use get_lip_sync_history to check progress.`,
20
+ ].join("\n"));
21
+ }
22
+ catch (error) {
23
+ return errorResult(error);
24
+ }
25
+ });
26
+ // 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.", {
28
+ projectSeq: z.number().describe("Project ID"),
29
+ spaceSeq: z.number().describe("Space ID"),
30
+ page: z.number().optional().default(0).describe("Page number"),
31
+ pageSize: z.number().optional().default(10).describe("Page size"),
32
+ }, async ({ projectSeq, spaceSeq, page, pageSize }) => {
33
+ try {
34
+ const result = await client.get(`/video-translator/api/v1/projects/${projectSeq}/spaces/${spaceSeq}/lip-sync/generated`, { page: String(page), pageSize: String(pageSize) });
35
+ if (result.content.length === 0) {
36
+ return textResult("No lip sync history found.");
37
+ }
38
+ const lines = result.content.map((g) => `[${g.createdAt}] #${g.generationSeq} - Type: ${g.type}, Status: ${g.status}`);
39
+ return textResult(`Lip Sync History:\n${lines.join("\n")}`);
40
+ }
41
+ catch (error) {
42
+ return errorResult(error);
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerProjectTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,277 @@
1
+ import { z } from "zod";
2
+ import { errorResult, textResult } from "../utils/formatting.js";
3
+ function formatProject(project) {
4
+ return [
5
+ `Project: ${project.title} (ID: ${project.projectSeq})`,
6
+ `Status: ${project.status}`,
7
+ `Type: ${project.type}`,
8
+ `Source: ${project.sourceLanguageCode} → ${(project.targetLanguageCodes || []).join(", ")}`,
9
+ `Created: ${project.createdAt}`,
10
+ `Updated: ${project.updatedAt}`,
11
+ ].join("\n");
12
+ }
13
+ export function registerProjectTools(server, client) {
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.number().describe("Space ID"),
17
+ mediaSeq: z.number().describe("Media sequence ID from upload_media"),
18
+ isVideoProject: z
19
+ .boolean()
20
+ .describe("true for video project, false for audio-only"),
21
+ sourceLanguageCode: z
22
+ .string()
23
+ .describe("Source language code (e.g., 'en')"),
24
+ targetLanguageCodes: z
25
+ .array(z.string())
26
+ .describe("Target language codes (e.g., ['ko', 'ja'])"),
27
+ numberOfSpeakers: z
28
+ .number()
29
+ .optional()
30
+ .describe("Number of speakers (auto-detected if omitted)"),
31
+ withLipSync: z
32
+ .boolean()
33
+ .optional()
34
+ .default(false)
35
+ .describe("Enable lip sync"),
36
+ preferredSpeedType: z
37
+ .enum(["GREEN", "RED"])
38
+ .optional()
39
+ .default("GREEN")
40
+ .describe("GREEN = standard speed, RED = expedited"),
41
+ }, async ({ spaceSeq, mediaSeq, isVideoProject, sourceLanguageCode, targetLanguageCodes, numberOfSpeakers, withLipSync, preferredSpeedType, }) => {
42
+ try {
43
+ // Initialize queue first (idempotent)
44
+ await client.put(`/video-translator/api/v1/projects/spaces/${spaceSeq}/queue`);
45
+ const body = {
46
+ mediaSeq,
47
+ isVideoProject,
48
+ sourceLanguageCode,
49
+ targetLanguageCodes,
50
+ preferredSpeedType,
51
+ };
52
+ if (numberOfSpeakers !== undefined)
53
+ body.numberOfSpeakers = numberOfSpeakers;
54
+ if (withLipSync)
55
+ body.withLipSync = withLipSync;
56
+ const result = await client.post(`/video-translator/api/v1/projects/spaces/${spaceSeq}/translate`, body);
57
+ const ids = result.startGenerateProjectIdList;
58
+ return textResult([
59
+ `Translation project(s) created successfully!`,
60
+ `Project IDs: ${ids.join(", ")}`,
61
+ `Use get_project with these IDs to check progress.`,
62
+ ].join("\n"));
63
+ }
64
+ catch (error) {
65
+ return errorResult(error);
66
+ }
67
+ });
68
+ // 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.number().describe("Space ID"),
71
+ offset: z.number().optional().default(0).describe("Page offset"),
72
+ size: z.number().optional().default(20).describe("Page size (max 100)"),
73
+ sort: z
74
+ .string()
75
+ .optional()
76
+ .default("update_date")
77
+ .describe("Sort field: 'update_date' or 'title'"),
78
+ type: z
79
+ .enum(["DUBBING", "LIP_SYNC"])
80
+ .optional()
81
+ .describe("Filter by project type"),
82
+ }, async ({ spaceSeq, offset, size, sort, type }) => {
83
+ try {
84
+ const query = {
85
+ offset: String(offset),
86
+ size: String(size),
87
+ sort,
88
+ };
89
+ if (type)
90
+ query.type = type;
91
+ const result = await client.get(`/video-translator/api/v1/projects/spaces/${spaceSeq}`, query);
92
+ if (result.content.length === 0) {
93
+ return textResult("No projects found.");
94
+ }
95
+ const header = `Projects (${result.totalElements} total, page ${result.number + 1}/${result.totalPages}):`;
96
+ const projects = result.content.map(formatProject).join("\n\n");
97
+ return textResult(`${header}\n\n${projects}`);
98
+ }
99
+ catch (error) {
100
+ return errorResult(error);
101
+ }
102
+ });
103
+ // 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.", {
105
+ projectSeq: z.number().describe("Project ID"),
106
+ spaceSeq: z.number().describe("Space ID"),
107
+ include: z
108
+ .array(z.enum([
109
+ "details",
110
+ "progress",
111
+ "video_info",
112
+ "download_info",
113
+ "used_features",
114
+ "retranslation_status",
115
+ ]))
116
+ .optional()
117
+ .default(["details"])
118
+ .describe("What info to include: details, progress, video_info, download_info, used_features, retranslation_status"),
119
+ }, async ({ projectSeq, spaceSeq, include }) => {
120
+ try {
121
+ const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${spaceSeq}`;
122
+ const basePathAlt = `/video-translator/api/v1/projects/${projectSeq}/space/${spaceSeq}`;
123
+ const sections = [];
124
+ const fetchers = {
125
+ details: async () => {
126
+ const project = await client.get(basePath);
127
+ return formatProject(project);
128
+ },
129
+ progress: async () => {
130
+ const progress = await client.get(`${basePathAlt}/progress`);
131
+ return [
132
+ `--- Progress ---`,
133
+ `Progress: ${progress.progress}%`,
134
+ `Remaining Time: ${progress.remainingTime}s`,
135
+ `Cancelable: ${progress.isCancelable}`,
136
+ ].join("\n");
137
+ },
138
+ video_info: async () => {
139
+ const info = await client.get(`${basePath}/video-info`);
140
+ return [
141
+ `--- Video Info ---`,
142
+ `Resolution: ${info.resolution} (${info.width}x${info.height})`,
143
+ `Duration: ${info.durationMs}ms`,
144
+ `Aspect Ratio: ${info.aspectRatio}`,
145
+ `Status: ${info.status}`,
146
+ ].join("\n");
147
+ },
148
+ download_info: async () => {
149
+ const info = await client.get(`${basePath}/download-info`);
150
+ return [
151
+ `--- Download Availability ---`,
152
+ `Video: ${info.videoAvailable}`,
153
+ `Audio: ${info.audioAvailable}`,
154
+ `SRT: ${info.srtAvailable}`,
155
+ ].join("\n");
156
+ },
157
+ used_features: async () => {
158
+ const features = await client.get(`${basePath}/used-features`);
159
+ return [
160
+ `--- Used Features ---`,
161
+ `Custom Dictionary: ${features.usedCustomDictionary}`,
162
+ `SRT Upload: ${features.usedSrtUpload}`,
163
+ ].join("\n");
164
+ },
165
+ retranslation_status: async () => {
166
+ const status = await client.get(`${basePath}/retranslation/status`);
167
+ return `--- Retranslation ---\nAvailable: ${status.retranslateAvailable}`;
168
+ },
169
+ };
170
+ const results = await Promise.allSettled(include.map(async (key) => {
171
+ const fetcher = fetchers[key];
172
+ if (fetcher)
173
+ return fetcher();
174
+ return `Unknown include: ${key}`;
175
+ }));
176
+ for (const result of results) {
177
+ if (result.status === "fulfilled") {
178
+ sections.push(result.value);
179
+ }
180
+ else {
181
+ sections.push(`Error: ${result.reason}`);
182
+ }
183
+ }
184
+ return textResult(sections.join("\n\n"));
185
+ }
186
+ catch (error) {
187
+ return errorResult(error);
188
+ }
189
+ });
190
+ // Tool: manage_project
191
+ server.tool("manage_project", "Manage a PERSO project: update title, update access permissions, delete, or cancel translation.", {
192
+ projectSeq: z.number().describe("Project ID"),
193
+ spaceSeq: z.number().describe("Space ID"),
194
+ action: z
195
+ .enum(["update_title", "update_access", "delete", "cancel"])
196
+ .describe("Action to perform"),
197
+ title: z
198
+ .string()
199
+ .optional()
200
+ .describe("New title (required for update_title)"),
201
+ accessPermission: z
202
+ .enum(["individual", "all"])
203
+ .optional()
204
+ .describe("Access level (required for update_access)"),
205
+ }, async ({ projectSeq, spaceSeq, action, title, accessPermission }) => {
206
+ try {
207
+ const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${spaceSeq}`;
208
+ switch (action) {
209
+ case "update_title": {
210
+ if (!title) {
211
+ return errorResult(new Error("title is required for update_title action"));
212
+ }
213
+ await client.patch(`${basePath}/title`, { newTitle: title });
214
+ return textResult(`Project title updated to: ${title}`);
215
+ }
216
+ case "update_access": {
217
+ if (!accessPermission) {
218
+ return errorResult(new Error("accessPermission is required for update_access action"));
219
+ }
220
+ await client.patch(`${basePath}/access`, undefined, {
221
+ permission: accessPermission,
222
+ });
223
+ return textResult(`Project access updated to: ${accessPermission}`);
224
+ }
225
+ case "delete": {
226
+ await client.delete(basePath);
227
+ return textResult(`Project ${projectSeq} deleted successfully.`);
228
+ }
229
+ case "cancel": {
230
+ await client.post(`${basePath}/cancel`);
231
+ return textResult(`Project ${projectSeq} translation cancelled.`);
232
+ }
233
+ }
234
+ }
235
+ catch (error) {
236
+ return errorResult(error);
237
+ }
238
+ });
239
+ // 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.", {
241
+ projectSeq: z.number().describe("Project ID"),
242
+ spaceSeq: z.number().describe("Space ID"),
243
+ action: z
244
+ .enum(["get_links", "export_history"])
245
+ .optional()
246
+ .default("get_links")
247
+ .describe("get_links: download URLs. export_history: past export records."),
248
+ }, async ({ projectSeq, spaceSeq, action }) => {
249
+ try {
250
+ if (action === "export_history") {
251
+ const basePath = `/video-translator/api/v1/projects/${projectSeq}/space/${spaceSeq}`;
252
+ const history = await client.get(`${basePath}/export-history`);
253
+ if (history.content.length === 0) {
254
+ return textResult("No export history found.");
255
+ }
256
+ const entries = history.content.map((e) => `[${e.createdAt}] ${e.exportType}: ${e.downloadUrl}`);
257
+ return textResult(`Export History:\n${entries.join("\n")}`);
258
+ }
259
+ // get_links
260
+ const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${spaceSeq}`;
261
+ const links = await client.get(`${basePath}/download`);
262
+ const parts = ["Download Links:"];
263
+ if (links.video)
264
+ parts.push(`Video: ${links.video}`);
265
+ if (links.audio)
266
+ parts.push(`Audio: ${links.audio}`);
267
+ if (links.srt)
268
+ parts.push(`SRT: ${links.srt}`);
269
+ if (parts.length === 1)
270
+ parts.push("No download links available yet.");
271
+ return textResult(parts.join("\n"));
272
+ }
273
+ catch (error) {
274
+ return errorResult(error);
275
+ }
276
+ });
277
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerScriptTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ import { errorResult, textResult } from "../utils/formatting.js";
3
+ export function registerScriptTools(server, client) {
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.", {
6
+ projectSeq: z.number().describe("Project ID"),
7
+ spaceSeq: z.number().describe("Space ID"),
8
+ cursorId: z
9
+ .number()
10
+ .optional()
11
+ .describe("Cursor for pagination (omit for first page)"),
12
+ size: z
13
+ .number()
14
+ .optional()
15
+ .default(50)
16
+ .describe("Number of sentences to return"),
17
+ }, async ({ projectSeq, spaceSeq, cursorId, size }) => {
18
+ try {
19
+ const basePath = `/video-translator/api/v1/projects/${projectSeq}/spaces/${spaceSeq}/script`;
20
+ const query = { size: String(size) };
21
+ if (cursorId !== undefined)
22
+ query.cursorId = String(cursorId);
23
+ const result = await client.get(basePath, query);
24
+ if (result.sentences.length === 0) {
25
+ return textResult("No sentences found.");
26
+ }
27
+ const lines = result.sentences.map((s) => [
28
+ `[#${s.sentenceSeq}] Speaker ${s.speakerSeq}`,
29
+ ` Original: ${s.originalText}`,
30
+ ` Translated: ${s.targetText}`,
31
+ ` Match Rate: ${s.matchingRate}%`,
32
+ s.audioUrl ? ` Audio: ${s.audioUrl}` : null,
33
+ ]
34
+ .filter(Boolean)
35
+ .join("\n"));
36
+ const footer = result.hasNext
37
+ ? `\n--- Has more sentences. Next cursor: ${result.nextCursorId} ---`
38
+ : "\n--- End of script ---";
39
+ return textResult(lines.join("\n\n") + footer);
40
+ }
41
+ catch (error) {
42
+ return errorResult(error);
43
+ }
44
+ });
45
+ // Tool: edit_sentence
46
+ server.tool("edit_sentence", "Edit a translated sentence in a PERSO project. Actions: translate (update text), generate_audio (regenerate audio), reset (revert to original), cancel (stop in-progress), temp_save (save draft), match_rewrite (get quality metrics).", {
47
+ projectSeq: z.number().describe("Project ID"),
48
+ sentenceSeq: z
49
+ .number()
50
+ .describe("Sentence or audio sentence sequence ID"),
51
+ action: z
52
+ .enum([
53
+ "translate",
54
+ "generate_audio",
55
+ "reset",
56
+ "cancel",
57
+ "temp_save",
58
+ "match_rewrite",
59
+ ])
60
+ .describe("Action to perform on the sentence"),
61
+ targetText: z
62
+ .string()
63
+ .optional()
64
+ .describe("New translation text (required for translate, generate_audio, temp_save, match_rewrite)"),
65
+ speakerSeq: z
66
+ .number()
67
+ .optional()
68
+ .describe("Speaker ID (for temp_save)"),
69
+ }, async ({ projectSeq, sentenceSeq, action, targetText, speakerSeq }) => {
70
+ try {
71
+ const basePath = `/video-translator/api/v1/project/${projectSeq}/audio-sentence/${sentenceSeq}`;
72
+ switch (action) {
73
+ case "translate": {
74
+ if (!targetText) {
75
+ return errorResult(new Error("targetText is required for translate"));
76
+ }
77
+ const result = await client.patch(basePath, { targetText });
78
+ return textResult(`Sentence translated. Match rate: ${result.matchingRate}%`);
79
+ }
80
+ case "generate_audio": {
81
+ if (!targetText) {
82
+ return errorResult(new Error("targetText is required for generate_audio"));
83
+ }
84
+ const result = await client.patch(`${basePath}/generate-audio`, { targetText });
85
+ return textResult([
86
+ `Audio generated.`,
87
+ `File: ${result.generateAudioFilePath}`,
88
+ `Match rate: ${result.matchingRate}%`,
89
+ ].join("\n"));
90
+ }
91
+ case "reset": {
92
+ await client.put(`${basePath}/reset`);
93
+ return textResult("Sentence reset to original translation.");
94
+ }
95
+ case "cancel": {
96
+ await client.put(`${basePath}/cancel`);
97
+ return textResult("Sentence translation cancelled.");
98
+ }
99
+ case "temp_save": {
100
+ const body = {};
101
+ if (targetText)
102
+ body.originalDraftText = targetText;
103
+ if (speakerSeq !== undefined)
104
+ body.speakerSeq = speakerSeq;
105
+ await client.post(`${basePath}/temp-save`, body);
106
+ return textResult("Draft saved temporarily.");
107
+ }
108
+ case "match_rewrite": {
109
+ if (!targetText) {
110
+ return errorResult(new Error("targetText is required for match_rewrite"));
111
+ }
112
+ const result = await client.post(`${basePath}/match-rewrite`, { targetText });
113
+ const parts = [`Match rate: ${result.matchingRate}%`];
114
+ if (result.rewriteText) {
115
+ parts.push(`Suggested rewrite: ${result.rewriteText}`);
116
+ }
117
+ return textResult(parts.join("\n"));
118
+ }
119
+ }
120
+ }
121
+ catch (error) {
122
+ return errorResult(error);
123
+ }
124
+ });
125
+ // Tool: request_proofread
126
+ server.tool("request_proofread", "Request AI proofreading for all sentences in a PERSO translation project.", {
127
+ projectSeq: z.number().describe("Project ID"),
128
+ spaceSeq: z.number().describe("Space ID"),
129
+ isLipSync: z
130
+ .boolean()
131
+ .optional()
132
+ .default(false)
133
+ .describe("Whether this is a lip sync project"),
134
+ preferredSpeedType: z
135
+ .enum(["GREEN", "RED"])
136
+ .optional()
137
+ .default("GREEN")
138
+ .describe("GREEN = standard, RED = expedited"),
139
+ }, async ({ projectSeq, spaceSeq, isLipSync, preferredSpeedType }) => {
140
+ try {
141
+ await client.post(`/video-translator/api/v1/project/${projectSeq}/space/${spaceSeq}/proofread`, { isLipSync, preferredSpeedType });
142
+ return textResult("Proofread request submitted successfully.");
143
+ }
144
+ catch (error) {
145
+ return errorResult(error);
146
+ }
147
+ });
148
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerSpaceTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { errorResult, textResult } from "../utils/formatting.js";
3
+ function formatSpace(space) {
4
+ return [
5
+ `Space: ${space.spaceName} (ID: ${space.spaceSeq})`,
6
+ `Plan: ${space.planName}`,
7
+ `Members: ${space.memberCount}/${space.seat}`,
8
+ `Role: ${space.memberRole}`,
9
+ ].join("\n");
10
+ }
11
+ export function registerSpaceTools(server, client) {
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
+ spaceSeq: z
14
+ .number()
15
+ .optional()
16
+ .describe("Space ID to get a specific space. Omit to list all spaces."),
17
+ }, async ({ spaceSeq }) => {
18
+ try {
19
+ if (spaceSeq !== undefined) {
20
+ const space = await client.get(`/portal/api/v1/spaces/${spaceSeq}`);
21
+ return textResult(formatSpace(space));
22
+ }
23
+ const spaces = await client.get("/portal/api/v1/spaces");
24
+ if (spaces.length === 0) {
25
+ return textResult("No spaces found.");
26
+ }
27
+ return textResult(`Spaces (${spaces.length}):\n\n${spaces.map(formatSpace).join("\n\n")}`);
28
+ }
29
+ catch (error) {
30
+ return errorResult(error);
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PersoApiClient } from "../client.js";
3
+ export declare function registerUsageTools(server: McpServer, client: PersoApiClient): void;
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
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.number().describe("Space ID"),
6
+ action: z
7
+ .enum(["plan_status", "estimate_quota", "queue_status"])
8
+ .default("plan_status")
9
+ .describe("plan_status: current quota and plan info. estimate_quota: estimate how much quota a media file will use. queue_status: check translation queue."),
10
+ mediaType: z
11
+ .enum(["VIDEO", "AUDIO"])
12
+ .optional()
13
+ .describe("Media type (required for estimate_quota)"),
14
+ durationMs: z
15
+ .number()
16
+ .optional()
17
+ .describe("Media duration in milliseconds (required for estimate_quota)"),
18
+ lipSync: z
19
+ .boolean()
20
+ .optional()
21
+ .describe("Whether lip sync is enabled (for estimate_quota)"),
22
+ width: z.number().optional().describe("Video width in pixels (for estimate_quota)"),
23
+ height: z.number().optional().describe("Video height in pixels (for estimate_quota)"),
24
+ targetLanguageSize: z
25
+ .number()
26
+ .optional()
27
+ .describe("Number of target languages (for estimate_quota)"),
28
+ }, async ({ spaceSeq, action, mediaType, durationMs, lipSync, width, height, targetLanguageSize }) => {
29
+ try {
30
+ const basePath = `/video-translator/api/v1/projects/spaces/${spaceSeq}`;
31
+ if (action === "plan_status") {
32
+ const status = await client.get(`${basePath}/plan/status`);
33
+ return textResult([
34
+ `Plan: ${status.planTier}`,
35
+ `Remaining Quota: ${status.remainingQuota}`,
36
+ `Reset: ${status.resetDateTime}`,
37
+ `Cancelled: ${status.isCancelled}`,
38
+ ].join("\n"));
39
+ }
40
+ if (action === "estimate_quota") {
41
+ const query = {};
42
+ if (mediaType)
43
+ query.mediaType = mediaType;
44
+ if (durationMs !== undefined)
45
+ query.durationMs = String(durationMs);
46
+ if (lipSync !== undefined)
47
+ query.lipSync = String(lipSync);
48
+ if (width !== undefined)
49
+ query.width = String(width);
50
+ if (height !== undefined)
51
+ query.height = String(height);
52
+ if (targetLanguageSize !== undefined)
53
+ query.targetLanguageSize = String(targetLanguageSize);
54
+ const estimate = await client.get(`${basePath}/media/quota`, query);
55
+ return textResult([
56
+ `Estimated Quota: ${estimate.estimatedQuota}`,
57
+ `Remaining Quota: ${estimate.remainingQuota}`,
58
+ `Available: ${estimate.isAvailable}`,
59
+ ].join("\n"));
60
+ }
61
+ // queue_status
62
+ const queue = await client.put(`${basePath}/queue`);
63
+ return textResult(`Queue: ${queue.usedQueueCount}/${queue.maxQueueCount} used`);
64
+ }
65
+ catch (error) {
66
+ return errorResult(error);
67
+ }
68
+ });
69
+ }
@@ -0,0 +1,129 @@
1
+ export interface Space {
2
+ spaceSeq: number;
3
+ spaceName: string;
4
+ memberCount: number;
5
+ seat: number;
6
+ planName: string;
7
+ memberRole: string;
8
+ }
9
+ export interface MediaUploadResult {
10
+ seq: number;
11
+ videoFilePath?: string;
12
+ audioFilePath?: string;
13
+ durationMs: number;
14
+ size: number;
15
+ }
16
+ export interface ExternalMetadata {
17
+ title: string;
18
+ durationMs: number;
19
+ thumbnailUrl: string;
20
+ }
21
+ export interface Project {
22
+ projectSeq: number;
23
+ title: string;
24
+ status: string;
25
+ sourceLanguageCode: string;
26
+ targetLanguageCodes: string[];
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ type: string;
30
+ progress?: number;
31
+ thumbnailUrl?: string;
32
+ }
33
+ export interface ProjectListResponse {
34
+ content: Project[];
35
+ totalElements: number;
36
+ totalPages: number;
37
+ size: number;
38
+ number: number;
39
+ }
40
+ export interface ProgressInfo {
41
+ progress: number;
42
+ remainingTime: number;
43
+ isCancelable: boolean;
44
+ }
45
+ export interface VideoInfo {
46
+ resolution: string;
47
+ durationMs: number;
48
+ status: string;
49
+ aspectRatio: string;
50
+ width: number;
51
+ height: number;
52
+ }
53
+ export interface DownloadLinks {
54
+ video?: string;
55
+ audio?: string;
56
+ srt?: string;
57
+ }
58
+ export interface DownloadInfo {
59
+ videoAvailable: boolean;
60
+ audioAvailable: boolean;
61
+ srtAvailable: boolean;
62
+ }
63
+ export interface Sentence {
64
+ sentenceSeq: number;
65
+ audioSentenceSeq: number;
66
+ originalText: string;
67
+ targetText: string;
68
+ matchingRate: number;
69
+ audioUrl: string;
70
+ speakerSeq: number;
71
+ }
72
+ export interface ScriptResponse {
73
+ sentences: Sentence[];
74
+ hasNext: boolean;
75
+ nextCursorId: number | null;
76
+ }
77
+ export interface Language {
78
+ languageCode: string;
79
+ languageName: string;
80
+ nativeName: string;
81
+ }
82
+ export interface PlanStatus {
83
+ remainingQuota: number;
84
+ planTier: string;
85
+ resetDateTime: string;
86
+ isCancelled: boolean;
87
+ }
88
+ export interface QuotaEstimate {
89
+ estimatedQuota: number;
90
+ remainingQuota: number;
91
+ isAvailable: boolean;
92
+ }
93
+ export interface QueueInfo {
94
+ usedQueueCount: number;
95
+ maxQueueCount: number;
96
+ }
97
+ export interface ExportHistoryEntry {
98
+ exportSeq: number;
99
+ exportType: string;
100
+ createdAt: string;
101
+ downloadUrl: string;
102
+ }
103
+ export interface LipSyncGeneration {
104
+ generationSeq: number;
105
+ type: string;
106
+ createdAt: string;
107
+ status: string;
108
+ }
109
+ export interface Feedback {
110
+ projectSeq: number;
111
+ rating: number;
112
+ averageRating: number;
113
+ count: number;
114
+ }
115
+ export interface FeaturedProject {
116
+ projectSeq: number;
117
+ title: string;
118
+ thumbnailUrl: string;
119
+ sourceLanguageCode: string;
120
+ targetLanguageCode: string;
121
+ videoUrl?: string;
122
+ }
123
+ export interface UsedFeatures {
124
+ usedCustomDictionary: boolean;
125
+ usedSrtUpload: boolean;
126
+ }
127
+ export interface RetranslationStatus {
128
+ retranslateAvailable: boolean;
129
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // PERSO API response types
2
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare function textResult(text: string): CallToolResult;
3
+ export declare function jsonResult(data: unknown): CallToolResult;
4
+ export declare function errorResult(error: unknown): CallToolResult;
@@ -0,0 +1,25 @@
1
+ import { PersoApiError } from "../client.js";
2
+ export function textResult(text) {
3
+ return { content: [{ type: "text", text }] };
4
+ }
5
+ export function jsonResult(data) {
6
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
7
+ }
8
+ export function errorResult(error) {
9
+ if (error instanceof PersoApiError) {
10
+ return {
11
+ content: [
12
+ {
13
+ type: "text",
14
+ text: `PERSO API Error [${error.code}] (HTTP ${error.status}): ${error.description}`,
15
+ },
16
+ ],
17
+ isError: true,
18
+ };
19
+ }
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ return {
22
+ content: [{ type: "text", text: `Error: ${message}` }],
23
+ isError: true,
24
+ };
25
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "perso-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for PERSO API - video translation and dubbing platform",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "perso-mcp-server": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepare": "npm run build",
16
+ "start": "node dist/index.js",
17
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "mcp-server",
22
+ "perso",
23
+ "video-translation",
24
+ "dubbing",
25
+ "ai"
26
+ ],
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.12.0",
33
+ "zod": "^3.24.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "typescript": "^5.7.0"
38
+ }
39
+ }