@zegazone_mcp/mcp 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,159 @@
1
+ import { STATUS_CODES } from "node:http";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import { callThirdParty, ensureDestructiveSafety, ensureIdempotency, ThirdPartyRequestError, } from "./api.js";
5
+ import { OP_TOOL_MAP } from "./ops.js";
6
+ import { formatToolDescription, THIRDPARTY_CALL_DESCRIPTION, } from "./tool-metadata.js";
7
+ import { TYPED_TOOL_SCHEMAS } from "./tool-schemas.js";
8
+ const GenericArgs = z.record(z.unknown()).optional();
9
+ /**
10
+ * MCP clients often send op fields at the top level (`{ "name": "X" }`). A plain
11
+ * `z.object({ args: z.record(...) })` strips unknown keys, so the API saw `{}` and
12
+ * `collections.create` failed validation — unrelated to error stringification.
13
+ */
14
+ function mergeNestedAndTopLevelArgs(parsed) {
15
+ const { args, ...top } = parsed;
16
+ const nested = typeof args === "object" && args !== null && !Array.isArray(args)
17
+ ? args
18
+ : {};
19
+ return { ...nested, ...top };
20
+ }
21
+ const thirdpartyCallInputSchema = z
22
+ .object({
23
+ op: z.string().min(1),
24
+ args: GenericArgs,
25
+ })
26
+ .catchall(z.unknown());
27
+ const explicitToolInputSchema = z
28
+ .object({
29
+ args: GenericArgs,
30
+ })
31
+ .catchall(z.unknown());
32
+ /** Never rely on the MCP SDK's `String(throwable)` for non-Error values (yields "[object Object]"). */
33
+ function formatToolFailure(err) {
34
+ if (err instanceof ThirdPartyRequestError)
35
+ return err.message;
36
+ if (err instanceof Error)
37
+ return err.message;
38
+ if (err !== null && typeof err === "object") {
39
+ try {
40
+ return JSON.stringify(err, jsonSerializeReplacer, 2);
41
+ }
42
+ catch {
43
+ return Object.prototype.toString.call(err);
44
+ }
45
+ }
46
+ return String(err);
47
+ }
48
+ function jsonSerializeReplacer(_key, value) {
49
+ if (value instanceof Error) {
50
+ return { name: value.name, message: value.message };
51
+ }
52
+ return value;
53
+ }
54
+ /** Some hosts stringify the wrong field; never return the useless "[object Object]" string. */
55
+ function coerceToolErrorText(err) {
56
+ let text = formatToolFailure(err);
57
+ if (typeof text !== "string") {
58
+ try {
59
+ text = JSON.stringify(text, jsonSerializeReplacer, 2);
60
+ }
61
+ catch {
62
+ text = "Unknown error (non-string tool message)";
63
+ }
64
+ }
65
+ if (text === "[object Object]" || text.trim() === "[object Object]") {
66
+ try {
67
+ text = JSON.stringify(err, jsonSerializeReplacer, 2);
68
+ }
69
+ catch {
70
+ text = "Unknown error (could not serialize)";
71
+ }
72
+ }
73
+ return text;
74
+ }
75
+ function toolErrorReturn(err) {
76
+ const text = coerceToolErrorText(err);
77
+ const structured = err instanceof ThirdPartyRequestError
78
+ ? {
79
+ httpStatus: err.payload.status,
80
+ httpStatusText: STATUS_CODES[err.payload.status] ?? "Unknown",
81
+ error: String(err.payload.error),
82
+ ...(err.payload.detail !== undefined ? { detail: String(err.payload.detail) } : {}),
83
+ ...(err.payload.field !== undefined ? { field: String(err.payload.field) } : {}),
84
+ ...(err.payload.need !== undefined
85
+ ? {
86
+ need: Array.isArray(err.payload.need)
87
+ ? err.payload.need.map(String)
88
+ : [String(err.payload.need)],
89
+ }
90
+ : {}),
91
+ }
92
+ : {
93
+ httpStatus: 0,
94
+ httpStatusText: "Error",
95
+ error: "tool_execution_error",
96
+ detail: text,
97
+ };
98
+ return {
99
+ content: [{ type: "text", text }],
100
+ structuredContent: structured,
101
+ isError: true,
102
+ };
103
+ }
104
+ export function createServer(config) {
105
+ const server = new McpServer({
106
+ name: "zegazone-thirdparty-mcp",
107
+ version: "2.0.0",
108
+ });
109
+ server.registerTool("thirdparty_call", {
110
+ description: `[Meta] ${THIRDPARTY_CALL_DESCRIPTION}`,
111
+ inputSchema: thirdpartyCallInputSchema,
112
+ }, async (parsed) => {
113
+ try {
114
+ const { op, ...rest } = parsed;
115
+ const payload = mergeNestedAndTopLevelArgs(rest);
116
+ const withSafety = ensureDestructiveSafety(op, payload, config.defaultDryRun);
117
+ const withIdempotency = ensureIdempotency(op, withSafety);
118
+ const response = await callThirdParty(config, op, withIdempotency);
119
+ return {
120
+ content: [
121
+ {
122
+ type: "text",
123
+ text: JSON.stringify(response, null, 2),
124
+ },
125
+ ],
126
+ };
127
+ }
128
+ catch (err) {
129
+ return toolErrorReturn(err);
130
+ }
131
+ });
132
+ for (const [toolName, op] of Object.entries(OP_TOOL_MAP)) {
133
+ const description = formatToolDescription(toolName, op);
134
+ const typedSchema = TYPED_TOOL_SCHEMAS[toolName];
135
+ server.registerTool(toolName, {
136
+ description,
137
+ inputSchema: (typedSchema ?? explicitToolInputSchema),
138
+ }, async (parsed) => {
139
+ try {
140
+ const input = mergeNestedAndTopLevelArgs(parsed);
141
+ const withSafety = ensureDestructiveSafety(op, input, config.defaultDryRun);
142
+ const withIdempotency = ensureIdempotency(op, withSafety);
143
+ const response = await callThirdParty(config, op, withIdempotency);
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: JSON.stringify(response, null, 2),
149
+ },
150
+ ],
151
+ };
152
+ }
153
+ catch (err) {
154
+ return toolErrorReturn(err);
155
+ }
156
+ });
157
+ }
158
+ return server;
159
+ }
@@ -0,0 +1,93 @@
1
+ import { readStoredCredentials, writeStoredCredentials } from "./token-store.js";
2
+ import { refreshAndPersist } from "./oauth-client.js";
3
+ const SKEW_MS = 120_000;
4
+ export function createAccessTokenHandle(opts) {
5
+ let memAccess = null;
6
+ let memExpiresAtMs = 0;
7
+ let inFlight = null;
8
+ let refreshInFlight = null;
9
+ function accessStillValid(expiresAtMs) {
10
+ return Date.now() < expiresAtMs - SKEW_MS;
11
+ }
12
+ async function resolveFromRefresh(stored) {
13
+ const next = await refreshAndPersist({
14
+ apiBase: opts.apiBase,
15
+ credentialsPath: opts.credentialsPath,
16
+ previous: stored,
17
+ timeoutMs: opts.timeoutMs,
18
+ writeStoredCredentials,
19
+ });
20
+ memAccess = next.access_token ?? null;
21
+ memExpiresAtMs = next.access_expires_at_ms ?? 0;
22
+ if (!memAccess)
23
+ throw new Error("Refresh succeeded but access_token missing");
24
+ return memAccess;
25
+ }
26
+ async function compute() {
27
+ const stored = await readStoredCredentials(opts.credentialsPath);
28
+ if (stored?.refresh_token) {
29
+ // Prefer the on-disk access_token over the memory cache when the file holds a
30
+ // different value. The MCP process is long-lived (Claude Desktop keeps it for
31
+ // days), and the credentials file can change underneath us — for example after
32
+ // a fresh `oauth-pair`, after an out-of-band scope grant that rotated the
33
+ // refresh row, or after a server-side secret rotation. Without this, the
34
+ // running process keeps replaying a stale-but-clock-valid token until expiry.
35
+ if (stored.access_token &&
36
+ stored.access_expires_at_ms &&
37
+ accessStillValid(stored.access_expires_at_ms)) {
38
+ if (memAccess !== stored.access_token) {
39
+ memAccess = stored.access_token;
40
+ memExpiresAtMs = stored.access_expires_at_ms;
41
+ }
42
+ return memAccess;
43
+ }
44
+ // File's access_token is missing or expired; fall back to memory if still valid
45
+ // (rare — memory is normally seeded from this same file), otherwise refresh.
46
+ if (memAccess && accessStillValid(memExpiresAtMs))
47
+ return memAccess;
48
+ return resolveFromRefresh(stored);
49
+ }
50
+ if (opts.legacyAccessToken)
51
+ return opts.legacyAccessToken;
52
+ throw new Error(`No OAuth credentials. Run: npm run oauth-pair (writes ${opts.credentialsPath}) or set ZEGA_ACCESS_TOKEN.`);
53
+ }
54
+ async function computeForced() {
55
+ const stored = await readStoredCredentials(opts.credentialsPath);
56
+ if (!stored?.refresh_token) {
57
+ if (opts.legacyAccessToken) {
58
+ throw new Error("Static ZEGA_ACCESS_TOKEN was rejected (401) and there is no refresh_token to renew it. " +
59
+ "Set a fresh ZEGA_ACCESS_TOKEN or run: npm run oauth-pair");
60
+ }
61
+ throw new Error(`No OAuth credentials to refresh. Run: npm run oauth-pair (writes ${opts.credentialsPath}).`);
62
+ }
63
+ return resolveFromRefresh(stored);
64
+ }
65
+ function invalidateMemory() {
66
+ memAccess = null;
67
+ memExpiresAtMs = 0;
68
+ }
69
+ async function getAccessToken() {
70
+ if (inFlight)
71
+ return inFlight;
72
+ const p = compute();
73
+ inFlight = p;
74
+ p.finally(() => {
75
+ if (inFlight === p)
76
+ inFlight = null;
77
+ });
78
+ return p;
79
+ }
80
+ async function forceRefreshAccessToken() {
81
+ invalidateMemory();
82
+ if (refreshInFlight)
83
+ return refreshInFlight;
84
+ const p = computeForced();
85
+ refreshInFlight = p;
86
+ p.finally(() => {
87
+ if (refreshInFlight === p)
88
+ refreshInFlight = null;
89
+ });
90
+ return p;
91
+ }
92
+ return { getAccessToken, forceRefreshAccessToken, invalidateMemory };
93
+ }
@@ -0,0 +1,48 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ export function defaultCredentialsPath() {
5
+ const override = process.env.ZEGA_CREDENTIALS_PATH?.trim();
6
+ if (override)
7
+ return path.resolve(override);
8
+ return path.join(os.homedir(), ".zegazone-mcp", "credentials.json");
9
+ }
10
+ export async function readStoredCredentials(filePath) {
11
+ try {
12
+ const raw = await fs.readFile(filePath, "utf8");
13
+ const parsed = JSON.parse(raw);
14
+ if (!parsed || typeof parsed !== "object")
15
+ return null;
16
+ const o = parsed;
17
+ if (o.version !== 1)
18
+ return null;
19
+ const client_id = typeof o.client_id === "string" ? o.client_id : "";
20
+ const refresh_token = typeof o.refresh_token === "string" ? o.refresh_token : "";
21
+ if (!client_id || !refresh_token)
22
+ return null;
23
+ const access_token = typeof o.access_token === "string" ? o.access_token : undefined;
24
+ const access_expires_at_ms = typeof o.access_expires_at_ms === "number" && Number.isFinite(o.access_expires_at_ms)
25
+ ? o.access_expires_at_ms
26
+ : undefined;
27
+ return {
28
+ version: 1,
29
+ client_id,
30
+ refresh_token,
31
+ access_token,
32
+ access_expires_at_ms,
33
+ };
34
+ }
35
+ catch (e) {
36
+ const code = e.code;
37
+ if (code === "ENOENT")
38
+ return null;
39
+ throw e;
40
+ }
41
+ }
42
+ export async function writeStoredCredentials(filePath, data) {
43
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
44
+ const tmp = `${filePath}.${process.pid}.tmp`;
45
+ const json = `${JSON.stringify(data, null, 2)}\n`;
46
+ await fs.writeFile(tmp, json, { encoding: "utf8", mode: 0o600 });
47
+ await fs.rename(tmp, filePath);
48
+ }
@@ -0,0 +1,128 @@
1
+ export const TOOL_CATEGORIES = {
2
+ ping: "Meta",
3
+ schema_get: "Meta",
4
+ operations_list: "Meta",
5
+ operation_describe: "Meta",
6
+ ui_state_get: "UI",
7
+ ui_state_set: "UI",
8
+ collections_list: "Collections",
9
+ collections_batch_get: "Collections",
10
+ collections_export: "Collections",
11
+ collections_create: "Collections",
12
+ collections_update: "Collections",
13
+ collections_delete: "Collections",
14
+ collections_restore: "Collections",
15
+ collections_reorder: "Collections",
16
+ collections_archive: "Collections",
17
+ collections_unarchive: "Collections",
18
+ collections_share_url: "Collections",
19
+ collections_publish: "Collections",
20
+ collections_unpublish: "Collections",
21
+ collections_like: "Social",
22
+ collections_unlike: "Social",
23
+ collections_liked_list: "Social",
24
+ collections_search: "Discovery",
25
+ collections_browse: "Discovery",
26
+ collections_stats_get: "Discovery",
27
+ aliases_list: "Social",
28
+ aliases_follow: "Social",
29
+ aliases_unfollow: "Social",
30
+ aliases_following_list: "Social",
31
+ profile_get: "Profile",
32
+ profile_update: "Profile",
33
+ collaborators_list: "Collaboration",
34
+ collaborators_invite: "Collaboration",
35
+ collaborators_revoke: "Collaboration",
36
+ collaborators_invites_list: "Collaboration",
37
+ collaborators_invites_received: "Collaboration",
38
+ collaborators_invite_accept: "Collaboration",
39
+ collaborators_invite_decline: "Collaboration",
40
+ media_list: "Media",
41
+ media_search: "Media",
42
+ media_batch_list: "Media",
43
+ media_download: "Media",
44
+ media_create: "Media",
45
+ media_update: "Media",
46
+ media_describe: "Media",
47
+ media_replace: "Media",
48
+ media_delete: "Media",
49
+ media_restore: "Media",
50
+ media_move: "Media",
51
+ media_copy: "Media",
52
+ media_reorder: "Media",
53
+ media_archive: "Media",
54
+ media_unarchive: "Media",
55
+ media_text_note_add: "Media",
56
+ media_text_note_get: "Media",
57
+ media_text_note_update: "Media",
58
+ media_text_note_delete: "Media",
59
+ };
60
+ /** When would I use this? — conversational descriptions for agents. */
61
+ export const TOOL_DESCRIPTIONS = {
62
+ ping: "Use when you need to verify the API token is valid before doing anything else. Quick health check returning your user id and OAuth client id.",
63
+ schema_get: "Use when building or debugging integrations and you need the full machine-readable contract (all operations, fields, scopes). Prefer named MCP tools for day-to-day work.",
64
+ operations_list: "Use when you want a compact index of every API operation and its OAuth scope before choosing what to call. Helpful for planning multi-step agent workflows.",
65
+ operation_describe: "Use when you need the request/response field documentation for one specific operation name from operations_list.",
66
+ ui_state_get: "Use when you need to know which collection and media item are currently open in the user's Zegazone UI — keeps agent actions aligned with what they see.",
67
+ ui_state_set: "Use when the agent should drive the app: open a collection, select a media item, or sync the UI to match your workflow.",
68
+ collections_list: "Use when you need to see what collections the user owns — names, tags, sharing level, thumbnails — before uploading media, publishing, or inviting collaborators.",
69
+ collections_batch_get: "Use when you already have a list of collection ids and need full metadata for several at once without listing everything.",
70
+ collections_export: "Use when the user wants a portable dump of a collection (metadata and media references) for backup or migration.",
71
+ collections_create: "Use when starting a new collection from scratch. Set name, tags, NSFW flag, and optional parent for sub-collections.",
72
+ collections_update: "Use when renaming a collection, changing tags, cover image, share level, or description on an existing owned collection.",
73
+ collections_delete: "Use when permanently or soft-deleting a collection the user no longer wants. Requires explicit confirm or dry_run for safety.",
74
+ collections_restore: "Use when undoing a soft-delete on a collection that was archived to the trash.",
75
+ collections_reorder: "Use when the user wants a specific sidebar order for their owned collections.",
76
+ collections_archive: "Use to hide a collection from default lists without deleting it — good for seasonal or paused projects.",
77
+ collections_unarchive: "Use to bring an archived collection back into the normal library view.",
78
+ collections_share_url: "Use when you need the public link to share with someone else (https://www.zegazone.com/{handle}/{slug}). Only works for published collections.",
79
+ collections_publish: "Use when the user wants a collection on the public web with a custom slug.",
80
+ collections_unpublish: "Use when a collection should no longer be publicly reachable.",
81
+ collections_like: "Use when the user wants to bookmark or endorse a public collection they discovered.",
82
+ collections_unlike: "Use to remove a like from a collection.",
83
+ collections_liked_list: "Use when revisiting collections the user has liked or building a favorites list.",
84
+ collections_search: "Use when discovering public content by keyword (min 3 characters). Great for 'find collections about X'.",
85
+ collections_browse: "Use for feed-style discovery without a search query — own library, shared with me, public, following, or liked.",
86
+ collections_stats_get: "Use when reporting how popular a collection is (likes, views, unique viewers).",
87
+ aliases_list: "Use before publishing under a channel handle or explaining which identities the user can publish as.",
88
+ aliases_follow: "Use when subscribing to another creator's channel so their public collections appear in following feeds.",
89
+ aliases_unfollow: "Use when stopping updates from a channel.",
90
+ aliases_following_list: "Use to show who the user follows or to pick an unfollow target.",
91
+ profile_get: "Use at the start of a session to learn username, display name, follower counts, bio, and aliases — personalize responses accordingly.",
92
+ profile_update: "Use when the user asks to change how they appear on Zegazone (display name or bio). Requires profile:write.",
93
+ collaborators_list: "Use to see who already has access to a restricted/private collection.",
94
+ collaborators_invite: "Use when the owner wants someone else to add or edit media on a restricted/private collection. Requires the invitee's Zegazone username.",
95
+ collaborators_revoke: "Use to remove a collaborator or cancel a pending invite.",
96
+ collaborators_invites_list: "Use to see outstanding invites the user sent.",
97
+ collaborators_invites_received: "Use when the user asks what collections they were invited to.",
98
+ collaborators_invite_accept: "Use after the user confirms they want access to a shared collection.",
99
+ collaborators_invite_decline: "Use when refusing a collaboration invite.",
100
+ media_list: "Use to list media inside a collection — the main way to see what's already uploaded before adding more.",
101
+ media_search: "Use when filtering media within one or more collections by name, tag, or type.",
102
+ media_batch_list: "Use when you have many media ids and need rows for all of them in one call.",
103
+ media_download: "Use when you need a time-limited download URL for a media item's main file.",
104
+ media_create: "Use when adding new content to a collection from a URL (image, video, website, etc.). Requires viewer type.",
105
+ media_update: "Use when changing title, description, position, tags, thumbnails, or metadata on existing media.",
106
+ media_describe: "Use when you need the full picture of a media item: metadata, viewer type, and resolved URLs for main/thumbnail/screenshot assets.",
107
+ media_replace: "Use when swapping the main file of a media item while keeping the same row id.",
108
+ media_delete: "Use when removing media the user no longer wants. Supports dry_run and confirm.",
109
+ media_restore: "Use when recovering soft-deleted media.",
110
+ media_move: "Use when reorganizing — move items to another owned collection, optionally at a position.",
111
+ media_copy: "Use when duplicating an item into another collection without re-uploading.",
112
+ media_reorder: "Use when setting explicit play order inside a collection.",
113
+ media_archive: "Use to hide a media item without deleting it.",
114
+ media_unarchive: "Use to restore archived media to the active list.",
115
+ media_text_note_add: "Use when adding an inline text/markdown note as a media row.",
116
+ media_text_note_get: "Use when reading the body of a text note media item.",
117
+ media_text_note_update: "Use when editing note content or cover thumbnail.",
118
+ media_text_note_delete: "Use when removing a text note. Supports dry_run and confirm.",
119
+ };
120
+ export const THIRDPARTY_CALL_DESCRIPTION = "Use when an operation exists in operations_list but has no dedicated MCP tool yet, or you need to pass rare/experimental fields. " +
121
+ "Pass the `op` string plus operation fields at the top level or under `args` (both merge). " +
122
+ "Destructive ops default to dry_run unless you set confirm:true. " +
123
+ "Prefer named tools when available — they have clearer descriptions and the same API behavior.";
124
+ export function formatToolDescription(toolName, op) {
125
+ const category = TOOL_CATEGORIES[toolName];
126
+ const when = TOOL_DESCRIPTIONS[toolName];
127
+ return `[${category}] ${when} (API op: ${op})`;
128
+ }
@@ -0,0 +1,146 @@
1
+ import { z } from "zod";
2
+ const viewerEnum = z.enum([
3
+ "video",
4
+ "audio",
5
+ "image",
6
+ "pdf",
7
+ "microsoft",
8
+ "web",
9
+ "markdown",
10
+ "text",
11
+ "code",
12
+ "file",
13
+ "website",
14
+ "html",
15
+ ]);
16
+ const shareAccessLevelEnum = z.enum(["private", "restricted", "registered", "public"]);
17
+ const tagsSchema = z
18
+ .array(z.string().min(1))
19
+ .max(10)
20
+ .optional()
21
+ .describe("Highly encouraged for discovery and organization (max 10).");
22
+ export const collectionsCreateSchema = z
23
+ .object({
24
+ args: z.record(z.unknown()).optional(),
25
+ name: z.string().min(1).describe("Collection display name."),
26
+ thumbnail_url: z
27
+ .string()
28
+ .url()
29
+ .optional()
30
+ .describe("Strongly recommended. Set a cover image URL to make the collection visually identifiable."),
31
+ description: z.string().max(4000).optional(),
32
+ is_nsfw: z.boolean().optional(),
33
+ share_access_level: shareAccessLevelEnum.optional(),
34
+ tags: tagsSchema,
35
+ searchable: z.boolean().optional().describe("When true, public collections may appear in search (default true)."),
36
+ share_allowed_usernames: z
37
+ .array(z.string().min(1))
38
+ .optional()
39
+ .describe("Usernames allowed when share_access_level is restricted."),
40
+ alias_id: z
41
+ .string()
42
+ .uuid()
43
+ .optional()
44
+ .describe("Profile alias UUID; sets created_with_username publish handle."),
45
+ playback_prefs: z.record(z.unknown()).optional().describe("JSON playback preferences."),
46
+ parent_collection_id: z.number().int().positive().optional(),
47
+ link_position: z.number().int().min(0).optional(),
48
+ link_description: z.string().max(4000).optional(),
49
+ })
50
+ .catchall(z.unknown());
51
+ export const collectionsUpdateSchema = z
52
+ .object({
53
+ args: z.record(z.unknown()).optional(),
54
+ collection_id: z.number().int().positive(),
55
+ name: z.string().min(1).optional(),
56
+ description: z.string().max(4000).nullable().optional(),
57
+ tags: tagsSchema,
58
+ thumbnail_url: z
59
+ .string()
60
+ .url()
61
+ .optional()
62
+ .describe("Strongly recommended. Set a cover image URL to make the collection visually identifiable."),
63
+ is_nsfw: z.boolean().optional(),
64
+ share_access_level: shareAccessLevelEnum.optional(),
65
+ position: z.number().int().min(0).optional(),
66
+ searchable: z.boolean().optional(),
67
+ share_allowed_usernames: z.array(z.string().min(1)).optional(),
68
+ alias_id: z.string().uuid().nullable().optional(),
69
+ playback_prefs: z.record(z.unknown()).nullable().optional(),
70
+ })
71
+ .catchall(z.unknown());
72
+ export const mediaCreateSchema = z
73
+ .object({
74
+ args: z.record(z.unknown()).optional(),
75
+ collection_id: z.number().int().positive(),
76
+ source_url: z.string().url(),
77
+ viewer: viewerEnum.describe("Hint for which viewer to use (required)."),
78
+ name: z.string().max(180).optional(),
79
+ description: z.string().max(4000).optional(),
80
+ position: z.number().int().min(0).optional(),
81
+ type: z.string().optional().describe("MIME type, e.g. image/jpeg"),
82
+ thumbnail_url: z
83
+ .string()
84
+ .url()
85
+ .optional()
86
+ .describe("Strongly recommended. Provide a thumbnail URL for visual identification in the carousel."),
87
+ tags: tagsSchema,
88
+ is_nsfw: z.boolean().optional(),
89
+ screenshot_url: z.string().url().optional(),
90
+ background_image_url: z.string().url().optional(),
91
+ locked_for_collaborators: z.boolean().optional(),
92
+ subcollection_collection_id: z.number().int().positive().nullable().optional(),
93
+ })
94
+ .catchall(z.unknown());
95
+ export const mediaUpdateSchema = z
96
+ .object({
97
+ args: z.record(z.unknown()).optional(),
98
+ media_id: z.number().int().positive(),
99
+ name: z.string().max(180).optional(),
100
+ description: z.string().max(4000).nullable().optional(),
101
+ position: z.number().int().min(0).optional(),
102
+ is_nsfw: z.boolean().optional(),
103
+ thumbnail_url: z
104
+ .string()
105
+ .url()
106
+ .nullable()
107
+ .optional()
108
+ .describe("Strongly recommended. Provide a thumbnail URL for visual identification in the carousel."),
109
+ viewer: viewerEnum.optional(),
110
+ type: z.string().optional(),
111
+ tags: tagsSchema,
112
+ text_note: z.string().max(524288).optional(),
113
+ display_mode: z.enum(["plain", "markdown", "code"]).optional(),
114
+ screenshot_url: z.string().url().nullable().optional(),
115
+ background_image_url: z.string().url().nullable().optional(),
116
+ locked_for_collaborators: z.boolean().optional(),
117
+ subcollection_collection_id: z.number().int().positive().nullable().optional(),
118
+ })
119
+ .catchall(z.unknown());
120
+ export const uiStateSetSchema = z
121
+ .object({
122
+ args: z.record(z.unknown()).optional(),
123
+ collection_id: z.number().int().positive().nullable().optional(),
124
+ media_id: z.number().int().positive().nullable().optional(),
125
+ source: z.enum(["agent", "user"]).optional(),
126
+ agent_id: z.string().nullable().optional(),
127
+ view_mode: z.enum(["carousel", "grid"]).nullable().optional(),
128
+ fullscreen: z.boolean().optional(),
129
+ collections_panel_open: z.boolean().optional(),
130
+ playback_position: z.number().min(0).nullable().optional(),
131
+ })
132
+ .catchall(z.unknown());
133
+ export const mediaDescribeSchema = z
134
+ .object({
135
+ args: z.record(z.unknown()).optional(),
136
+ media_id: z.number().int().positive(),
137
+ })
138
+ .catchall(z.unknown());
139
+ export const TYPED_TOOL_SCHEMAS = {
140
+ collections_create: collectionsCreateSchema,
141
+ collections_update: collectionsUpdateSchema,
142
+ media_create: mediaCreateSchema,
143
+ media_update: mediaUpdateSchema,
144
+ ui_state_set: uiStateSetSchema,
145
+ media_describe: mediaDescribeSchema,
146
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@zegazone_mcp/mcp",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "description": "MCP server wrapper for Zegazone thirdparty-v1 API",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "zegazone-mcp": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist/",
14
+ "README.md",
15
+ "scripts/oauth-pair.mjs"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json && node scripts/add-shebang.mjs",
19
+ "start": "node dist/index.js",
20
+ "dev": "tsx src/index.ts",
21
+ "oauth-pair": "node scripts/oauth-pair.mjs",
22
+ "smoke": "node tests/smoke.mjs",
23
+ "smoke:full": "node tests/smoke-thirdparty-v1.mjs",
24
+ "test:error-format": "npm run build && node tests/error-format.mjs",
25
+ "test:token-provider": "npm run build && node tests/token-provider-file-priority.mjs",
26
+ "test": "npm run build && node tests/error-format.mjs && node tests/token-provider-file-priority.mjs"
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.12.0",
30
+ "zod": "^3.24.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.10.2",
34
+ "tsx": "^4.19.2",
35
+ "typescript": "^5.8.3"
36
+ }
37
+ }