@yoonion/mimi-seed-mcp 0.19.13 → 0.19.17

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.
Files changed (54) hide show
  1. package/README.md +2 -1
  2. package/assets/agent-guide.md +44 -7
  3. package/dist/appstore/tools.js +83 -29
  4. package/dist/auth/browser.d.ts +2 -0
  5. package/dist/auth/browser.js +4 -0
  6. package/dist/auth/cli.js +33 -8
  7. package/dist/auth/google-auth.d.ts +26 -5
  8. package/dist/auth/google-auth.js +133 -47
  9. package/dist/auth/scopes.d.ts +5 -0
  10. package/dist/auth/scopes.js +8 -0
  11. package/dist/auth/youtube-channel.d.ts +8 -0
  12. package/dist/auth/youtube-channel.js +26 -0
  13. package/dist/googleads/errors.d.ts +2 -0
  14. package/dist/googleads/errors.js +31 -0
  15. package/dist/googleads/tools.d.ts +7 -0
  16. package/dist/googleads/tools.js +89 -46
  17. package/dist/helpers.d.ts +1 -1
  18. package/dist/helpers.js +10 -10
  19. package/dist/instagram/browser-login.d.ts +13 -0
  20. package/dist/instagram/browser-login.js +125 -0
  21. package/dist/instagram/setup.d.ts +1 -1
  22. package/dist/instagram/setup.js +5 -2
  23. package/dist/lib/googleapis-lite.d.ts +2 -0
  24. package/dist/lib/googleapis-lite.js +3 -0
  25. package/dist/lib/store-create-result.d.ts +221 -0
  26. package/dist/lib/store-create-result.js +38 -0
  27. package/dist/playstore/tools.d.ts +2 -0
  28. package/dist/playstore/tools.js +23 -8
  29. package/dist/registers/appstore.js +3 -47
  30. package/dist/registers/auth.js +30 -6
  31. package/dist/registers/googleads.js +5 -2
  32. package/dist/registers/playstore.js +3 -16
  33. package/dist/registers/video.js +11 -5
  34. package/dist/registers/youtube.d.ts +2 -0
  35. package/dist/registers/youtube.js +89 -0
  36. package/dist/server.js +2 -0
  37. package/dist/social/setup-cli.js +66 -19
  38. package/dist/threads/browser-login.d.ts +13 -0
  39. package/dist/threads/browser-login.js +125 -0
  40. package/dist/threads/config.js +2 -4
  41. package/dist/threads/setup.d.ts +1 -1
  42. package/dist/threads/setup.js +7 -4
  43. package/dist/video/schemas.d.ts +12 -12
  44. package/dist/video/youtube-publish.d.ts +14 -0
  45. package/dist/video/youtube-publish.js +8 -0
  46. package/dist/youtube/comments.d.ts +156 -0
  47. package/dist/youtube/comments.js +158 -0
  48. package/dist/youtube/insights.d.ts +82 -0
  49. package/dist/youtube/insights.js +155 -0
  50. package/dist/youtube/tools.d.ts +174 -0
  51. package/dist/youtube/tools.js +292 -0
  52. package/package.json +5 -3
  53. package/scripts/googleads-report.mjs +32 -0
  54. package/tool-manifest.json +18 -1
@@ -0,0 +1,174 @@
1
+ import type { OAuth2Client } from 'google-auth-library';
2
+ import type { youtube_v3 } from '../lib/googleapis-lite.js';
3
+ export interface YouTubeWriteTarget {
4
+ expectedChannelId: string;
5
+ videoId: string;
6
+ }
7
+ export interface UpdateYouTubeMetadataInput extends YouTubeWriteTarget {
8
+ title?: string;
9
+ description?: string;
10
+ tags?: string[];
11
+ confirm?: boolean;
12
+ }
13
+ export declare function updateYouTubeVideoMetadata(auth: OAuth2Client, input: UpdateYouTubeMetadataInput): Promise<{
14
+ videoId: string;
15
+ channel: {
16
+ id: string;
17
+ title: string;
18
+ };
19
+ before: {
20
+ title: string;
21
+ description: string;
22
+ tags: string[];
23
+ categoryId: string;
24
+ defaultLanguage: string | null;
25
+ };
26
+ after: youtube_v3.Schema$VideoSnippet;
27
+ status: "preview";
28
+ } | {
29
+ applied: {
30
+ title: string | null;
31
+ description: string | null;
32
+ tags: string[] | null;
33
+ };
34
+ videoId: string;
35
+ channel: {
36
+ id: string;
37
+ title: string;
38
+ };
39
+ before: {
40
+ title: string;
41
+ description: string;
42
+ tags: string[];
43
+ categoryId: string;
44
+ defaultLanguage: string | null;
45
+ };
46
+ after: youtube_v3.Schema$VideoSnippet;
47
+ status: "updated";
48
+ }>;
49
+ export interface SetYouTubeThumbnailInput extends YouTubeWriteTarget {
50
+ filePath: string;
51
+ confirm?: boolean;
52
+ }
53
+ export declare function setYouTubeThumbnail(auth: OAuth2Client, input: SetYouTubeThumbnailInput): Promise<{
54
+ videoId: string;
55
+ channel: {
56
+ id: string;
57
+ title: string;
58
+ };
59
+ title: string | null;
60
+ file: {
61
+ size: number;
62
+ mimeType: string;
63
+ path: string;
64
+ };
65
+ status: "preview";
66
+ } | {
67
+ thumbnails: youtube_v3.Schema$ThumbnailDetails[];
68
+ videoId: string;
69
+ channel: {
70
+ id: string;
71
+ title: string;
72
+ };
73
+ title: string | null;
74
+ file: {
75
+ size: number;
76
+ mimeType: string;
77
+ path: string;
78
+ };
79
+ status: "updated";
80
+ }>;
81
+ export interface ScheduleYouTubeVideoInput extends YouTubeWriteTarget {
82
+ publishAt: string;
83
+ confirmVisible?: boolean;
84
+ }
85
+ export declare function scheduleYouTubeVideo(auth: OAuth2Client, input: ScheduleYouTubeVideoInput): Promise<{
86
+ videoId: string;
87
+ channel: {
88
+ id: string;
89
+ title: string;
90
+ };
91
+ title: string | null;
92
+ before: {
93
+ privacyStatus: string;
94
+ publishAt: string | null;
95
+ };
96
+ after: {
97
+ privacyStatus: string;
98
+ publishAt: string;
99
+ };
100
+ status: "preview";
101
+ } | {
102
+ applied: {
103
+ privacyStatus: string;
104
+ publishAt: string;
105
+ };
106
+ videoId: string;
107
+ channel: {
108
+ id: string;
109
+ title: string;
110
+ };
111
+ title: string | null;
112
+ before: {
113
+ privacyStatus: string;
114
+ publishAt: string | null;
115
+ };
116
+ after: {
117
+ privacyStatus: string;
118
+ publishAt: string;
119
+ };
120
+ status: "scheduled";
121
+ }>;
122
+ export declare function getYouTubeChannel(auth: OAuth2Client, expectedChannelId?: string): Promise<{
123
+ channelId: string;
124
+ title: string;
125
+ description: string | null;
126
+ customUrl: string | null;
127
+ publishedAt: string | null;
128
+ uploadsPlaylistId: string | null;
129
+ statistics: youtube_v3.Schema$ChannelStatistics | null;
130
+ }>;
131
+ export declare function listYouTubeVideos(auth: OAuth2Client, input: {
132
+ expectedChannelId?: string;
133
+ maxResults?: number;
134
+ pageToken?: string;
135
+ }): Promise<{
136
+ channel: {
137
+ id: string;
138
+ title: string;
139
+ };
140
+ videos: {
141
+ videoId: string | null | undefined;
142
+ title: string | null;
143
+ publishedAt: string | null;
144
+ privacyStatus: string | null;
145
+ duration: string | null;
146
+ statistics: youtube_v3.Schema$VideoStatistics | null;
147
+ }[];
148
+ nextPageToken: string | null;
149
+ }>;
150
+ export type AnalyticsDimension = 'total' | 'day' | 'video';
151
+ export interface YouTubeAnalyticsInput {
152
+ expectedChannelId?: string;
153
+ startDate: string;
154
+ endDate: string;
155
+ dimension?: AnalyticsDimension;
156
+ maxResults?: number;
157
+ startIndex?: number;
158
+ }
159
+ export declare function getYouTubeAnalyticsReport(auth: OAuth2Client, input: YouTubeAnalyticsInput): Promise<{
160
+ channel: {
161
+ id: string;
162
+ title: string;
163
+ };
164
+ requestedPeriod: {
165
+ startDate: string;
166
+ endDate: string;
167
+ };
168
+ dimension: AnalyticsDimension;
169
+ columnHeaders: import("googleapis/build/src/apis/youtubeAnalytics/v2.js").youtubeAnalytics_v2.Schema$ResultTableColumnHeader[];
170
+ rows: any[][];
171
+ startIndex: number;
172
+ maxResults: number;
173
+ note: string;
174
+ }>;
@@ -0,0 +1,292 @@
1
+ import { google } from '../lib/googleapis-lite.js';
2
+ import { friendlyGoogleError, googleErrorDetail } from '../lib/google-errors.js';
3
+ import { verifyYouTubeChannel } from '../auth/youtube-channel.js';
4
+ import { createReadStream, openSync, readSync, closeSync, statSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ function youtubeError(error) {
7
+ const friendly = friendlyGoogleError(error);
8
+ const detail = googleErrorDetail(error);
9
+ return detail && !friendly.message.includes(detail)
10
+ ? new Error(`${friendly.message}\nGoogle API: ${detail}`, { cause: error })
11
+ : friendly;
12
+ }
13
+ async function ownedVideo(auth, expectedChannelId, videoId) {
14
+ if (!expectedChannelId)
15
+ throw new Error('expectedChannelId is required for YouTube writes.');
16
+ const channel = await verifyYouTubeChannel(auth, expectedChannelId);
17
+ try {
18
+ const response = await google.youtube({ version: 'v3', auth }).videos.list({
19
+ part: ['snippet', 'status'], id: [videoId],
20
+ });
21
+ const video = response.data.items?.[0];
22
+ if (!video || video.id !== videoId)
23
+ throw new Error(`YouTube video not found: ${videoId}`);
24
+ if (video.snippet?.channelId !== channel.id)
25
+ throw new Error('Video does not belong to the authenticated YouTube channel.');
26
+ return { channel, video };
27
+ }
28
+ catch (error) {
29
+ throw youtubeError(error);
30
+ }
31
+ }
32
+ export async function updateYouTubeVideoMetadata(auth, input) {
33
+ if (input.title === undefined && input.description === undefined && input.tags === undefined) {
34
+ throw new Error('Specify at least one of title, description, or tags.');
35
+ }
36
+ const { channel, video } = await ownedVideo(auth, input.expectedChannelId, input.videoId);
37
+ const old = video.snippet;
38
+ if (!old?.title || !old.categoryId)
39
+ throw new Error('YouTube did not return the current title/categoryId needed to preserve video metadata.');
40
+ const title = input.title ?? old.title;
41
+ const description = input.description ?? old.description ?? '';
42
+ const tags = input.tags ?? old.tags ?? [];
43
+ if (!title.trim() || title.length > 100 || /[<>]/.test(title))
44
+ throw new Error('title must be 1–100 characters and cannot contain < or >.');
45
+ if (Buffer.byteLength(description, 'utf8') > 5000 || /[<>]/.test(description))
46
+ throw new Error('description must be <=5000 UTF-8 bytes and cannot contain < or >.');
47
+ if (tags.some((tag) => !tag.trim()) || tags.map((tag) => tag.includes(' ') ? `"${tag}"` : tag).join(',').length > 500) {
48
+ throw new Error('tags must be nonempty and fit the YouTube 500-character combined limit.');
49
+ }
50
+ const snippet = {
51
+ title, description, categoryId: old.categoryId, tags,
52
+ ...(old.defaultLanguage ? { defaultLanguage: old.defaultLanguage } : {}),
53
+ };
54
+ const preview = {
55
+ videoId: input.videoId, channel, before: {
56
+ title: old.title, description: old.description ?? '', tags: old.tags ?? [],
57
+ categoryId: old.categoryId, defaultLanguage: old.defaultLanguage ?? null,
58
+ }, after: snippet,
59
+ };
60
+ if (input.confirm !== true)
61
+ return { status: 'preview', ...preview };
62
+ try {
63
+ const response = await google.youtube({ version: 'v3', auth }).videos.update({
64
+ part: ['snippet'], requestBody: { id: input.videoId, snippet },
65
+ });
66
+ return { status: 'updated', ...preview, applied: {
67
+ title: response.data.snippet?.title ?? null,
68
+ description: response.data.snippet?.description ?? null,
69
+ tags: response.data.snippet?.tags ?? null,
70
+ } };
71
+ }
72
+ catch (error) {
73
+ throw youtubeError(error);
74
+ }
75
+ }
76
+ function inspectThumbnail(filePath) {
77
+ if (!path.isAbsolute(filePath))
78
+ throw new Error('filePath must be an absolute path.');
79
+ const ext = path.extname(filePath).toLowerCase();
80
+ if (!['.jpg', '.jpeg', '.png'].includes(ext))
81
+ throw new Error('Thumbnail must be a .jpg, .jpeg, or .png file.');
82
+ let size;
83
+ const header = Buffer.alloc(8);
84
+ try {
85
+ const stat = statSync(filePath);
86
+ if (!stat.isFile())
87
+ throw new Error('Thumbnail path must name a file.');
88
+ size = stat.size;
89
+ const fd = openSync(filePath, 'r');
90
+ try {
91
+ readSync(fd, header, 0, 8, 0);
92
+ }
93
+ finally {
94
+ closeSync(fd);
95
+ }
96
+ }
97
+ catch (error) {
98
+ if (error instanceof Error && error.message === 'Thumbnail path must name a file.')
99
+ throw error;
100
+ throw new Error(`Cannot read thumbnail file: ${filePath}`, { cause: error });
101
+ }
102
+ if (size < 8 || size > 50 * 1024 * 1024)
103
+ throw new Error('Thumbnail file must be nonempty and at most 50 MB.');
104
+ const png = header.equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
105
+ const jpeg = header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff;
106
+ if ((ext === '.png' && !png) || (ext !== '.png' && !jpeg))
107
+ throw new Error('Thumbnail contents do not match the JPEG/PNG file extension.');
108
+ return { size, mimeType: png ? 'image/png' : 'image/jpeg' };
109
+ }
110
+ export async function setYouTubeThumbnail(auth, input) {
111
+ const file = inspectThumbnail(input.filePath);
112
+ const { channel, video } = await ownedVideo(auth, input.expectedChannelId, input.videoId);
113
+ const preview = { videoId: input.videoId, channel, title: video.snippet?.title ?? null,
114
+ file: { path: input.filePath, ...file } };
115
+ if (input.confirm !== true)
116
+ return { status: 'preview', ...preview };
117
+ try {
118
+ const response = await google.youtube({ version: 'v3', auth }).thumbnails.set({
119
+ videoId: input.videoId, media: { mimeType: file.mimeType, body: createReadStream(input.filePath) },
120
+ });
121
+ return { status: 'updated', ...preview, thumbnails: response.data.items ?? [] };
122
+ }
123
+ catch (error) {
124
+ throw youtubeError(error);
125
+ }
126
+ }
127
+ function futureTimestamp(value) {
128
+ const parts = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-](\d{2}):(\d{2}))$/.exec(value);
129
+ if (!parts) {
130
+ throw new Error('publishAt must be an RFC3339 timestamp with timezone (Z or ±HH:MM).');
131
+ }
132
+ const [year, month, day, hour, minute, second] = parts.slice(1, 7).map(Number);
133
+ const date = new Date(Date.UTC(year, month - 1, day));
134
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day ||
135
+ hour > 23 || minute > 59 || second > 59 || (parts[7] !== 'Z' && (Number(parts[8]) > 23 || Number(parts[9]) > 59))) {
136
+ throw new Error('publishAt contains an invalid calendar date, time, or timezone offset.');
137
+ }
138
+ const parsed = new Date(value);
139
+ if (!Number.isFinite(parsed.getTime()) || parsed.getTime() <= Date.now() + 60_000) {
140
+ throw new Error('publishAt must be a valid timestamp at least one minute in the future.');
141
+ }
142
+ return parsed.toISOString();
143
+ }
144
+ export async function scheduleYouTubeVideo(auth, input) {
145
+ const publishAt = futureTimestamp(input.publishAt);
146
+ const { channel, video } = await ownedVideo(auth, input.expectedChannelId, input.videoId);
147
+ const old = video.status;
148
+ if (old?.privacyStatus !== 'private')
149
+ throw new Error('Only a currently private video can be scheduled.');
150
+ if (old.uploadStatus === 'deleted' || old.uploadStatus === 'failed' || old.uploadStatus === 'rejected') {
151
+ throw new Error(`Video upload status ${old.uploadStatus} cannot be scheduled.`);
152
+ }
153
+ const status = {
154
+ privacyStatus: 'private', publishAt,
155
+ ...(old.embeddable !== undefined && { embeddable: old.embeddable }),
156
+ ...(old.license && { license: old.license }),
157
+ ...(old.publicStatsViewable !== undefined && { publicStatsViewable: old.publicStatsViewable }),
158
+ ...(old.selfDeclaredMadeForKids !== undefined && { selfDeclaredMadeForKids: old.selfDeclaredMadeForKids }),
159
+ ...(old.containsSyntheticMedia !== undefined && { containsSyntheticMedia: old.containsSyntheticMedia }),
160
+ };
161
+ const preview = { videoId: input.videoId, channel, title: video.snippet?.title ?? null,
162
+ before: { privacyStatus: old.privacyStatus, publishAt: old.publishAt ?? null },
163
+ after: { privacyStatus: 'private', publishAt } };
164
+ if (input.confirmVisible !== true)
165
+ return { status: 'preview', ...preview };
166
+ try {
167
+ futureTimestamp(input.publishAt);
168
+ await google.youtube({ version: 'v3', auth }).videos.update({ part: ['status'],
169
+ requestBody: { id: input.videoId, status } });
170
+ let check;
171
+ try {
172
+ check = await ownedVideo(auth, input.expectedChannelId, input.videoId);
173
+ }
174
+ catch (error) {
175
+ throw new Error('Scheduling update was sent but readback failed. Inspect youtube_get_video_status before retrying.', { cause: error });
176
+ }
177
+ if (check.video.status?.privacyStatus !== 'private' || !check.video.status?.publishAt ||
178
+ new Date(check.video.status.publishAt).getTime() !== new Date(publishAt).getTime()) {
179
+ throw new Error('Scheduling update was sent but readback did not match. Inspect youtube_get_video_status before retrying.');
180
+ }
181
+ return { status: 'scheduled', ...preview,
182
+ applied: { privacyStatus: check.video.status?.privacyStatus ?? null,
183
+ publishAt: check.video.status?.publishAt ?? null } };
184
+ }
185
+ catch (error) {
186
+ throw youtubeError(error);
187
+ }
188
+ }
189
+ export async function getYouTubeChannel(auth, expectedChannelId) {
190
+ const channel = await verifyYouTubeChannel(auth, expectedChannelId);
191
+ try {
192
+ const response = await google.youtube({ version: 'v3', auth }).channels.list({
193
+ part: ['snippet', 'contentDetails', 'statistics'], id: [channel.id], maxResults: 1,
194
+ });
195
+ const item = response.data.items?.[0];
196
+ if (!item || item.id !== channel.id)
197
+ throw new Error('Verified YouTube channel was not returned by channels.list.');
198
+ return {
199
+ channelId: channel.id,
200
+ title: item.snippet?.title ?? channel.title,
201
+ description: item.snippet?.description ?? null,
202
+ customUrl: item.snippet?.customUrl ?? null,
203
+ publishedAt: item.snippet?.publishedAt ?? null,
204
+ uploadsPlaylistId: item.contentDetails?.relatedPlaylists?.uploads ?? null,
205
+ statistics: item.statistics ?? null,
206
+ };
207
+ }
208
+ catch (error) {
209
+ throw youtubeError(error);
210
+ }
211
+ }
212
+ export async function listYouTubeVideos(auth, input) {
213
+ const channel = await getYouTubeChannel(auth, input.expectedChannelId);
214
+ if (!channel.uploadsPlaylistId)
215
+ throw new Error('Authenticated YouTube channel has no uploads playlist.');
216
+ const maxResults = input.maxResults ?? 25;
217
+ if (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 50)
218
+ throw new Error('maxResults must be 1–50.');
219
+ try {
220
+ const page = await google.youtube({ version: 'v3', auth }).playlistItems.list({
221
+ part: ['contentDetails'], playlistId: channel.uploadsPlaylistId,
222
+ maxResults, ...(input.pageToken ? { pageToken: input.pageToken } : {}),
223
+ });
224
+ const ids = (page.data.items ?? []).map((item) => item.contentDetails?.videoId).filter((id) => !!id);
225
+ if (ids.length === 0)
226
+ return { channel: { id: channel.channelId, title: channel.title }, videos: [], nextPageToken: page.data.nextPageToken ?? null };
227
+ const details = await google.youtube({ version: 'v3', auth }).videos.list({
228
+ part: ['snippet', 'contentDetails', 'status', 'statistics'], id: ids,
229
+ });
230
+ const byId = new Map((details.data.items ?? []).map((video) => [video.id, video]));
231
+ return {
232
+ channel: { id: channel.channelId, title: channel.title },
233
+ videos: ids.map((id) => byId.get(id)).filter((video) => video?.snippet?.channelId === channel.channelId).map((video) => ({
234
+ videoId: video.id, title: video.snippet?.title ?? null,
235
+ publishedAt: video.snippet?.publishedAt ?? null,
236
+ privacyStatus: video.status?.privacyStatus ?? null,
237
+ duration: video.contentDetails?.duration ?? null,
238
+ statistics: video.statistics ?? null,
239
+ })),
240
+ nextPageToken: page.data.nextPageToken ?? null,
241
+ };
242
+ }
243
+ catch (error) {
244
+ throw youtubeError(error);
245
+ }
246
+ }
247
+ function assertIsoDate(value, field) {
248
+ const parsed = /^\d{4}-\d{2}-\d{2}$/.test(value) ? new Date(`${value}T00:00:00Z`) : null;
249
+ if (!parsed || !Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {
250
+ throw new Error(`${field} must be a valid YYYY-MM-DD date.`);
251
+ }
252
+ }
253
+ export async function getYouTubeAnalyticsReport(auth, input) {
254
+ assertIsoDate(input.startDate, 'startDate');
255
+ assertIsoDate(input.endDate, 'endDate');
256
+ if (input.startDate > input.endDate)
257
+ throw new Error('startDate must be on or before endDate.');
258
+ const dimension = input.dimension ?? 'day';
259
+ if (!['total', 'day', 'video'].includes(dimension))
260
+ throw new Error('dimension must be total, day, or video.');
261
+ const maxResults = input.maxResults ?? (dimension === 'video' ? 50 : 200);
262
+ const startIndex = input.startIndex ?? 1;
263
+ if (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 200)
264
+ throw new Error('maxResults must be 1–200.');
265
+ if (!Number.isInteger(startIndex) || startIndex < 1 || startIndex > 10_000)
266
+ throw new Error('startIndex must be 1–10000.');
267
+ const channel = await verifyYouTubeChannel(auth, input.expectedChannelId);
268
+ try {
269
+ const report = await google.youtubeAnalytics({ version: 'v2', auth }).reports.query({
270
+ ids: `channel==${channel.id}`,
271
+ startDate: input.startDate,
272
+ endDate: input.endDate,
273
+ metrics: 'views,estimatedMinutesWatched,averageViewDuration,subscribersGained',
274
+ ...(dimension !== 'total' ? { dimensions: dimension } : {}),
275
+ ...(dimension === 'video' ? { sort: '-views' } : dimension === 'day' ? { sort: 'day' } : {}),
276
+ maxResults, startIndex,
277
+ });
278
+ return {
279
+ channel,
280
+ requestedPeriod: { startDate: input.startDate, endDate: input.endDate },
281
+ dimension,
282
+ columnHeaders: report.data.columnHeaders ?? [],
283
+ rows: report.data.rows ?? [],
284
+ startIndex,
285
+ maxResults,
286
+ note: 'YouTube Analytics can omit recent days until all requested metrics are available.',
287
+ };
288
+ }
289
+ catch (error) {
290
+ throw youtubeError(error);
291
+ }
292
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.19.13",
4
- "description": "Mimi Seed MCP server \u2014 Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
3
+ "version": "0.19.17",
4
+ "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mimi-seed-mcp": "dist/index.js",
@@ -20,11 +20,13 @@
20
20
  },
21
21
  "files": [
22
22
  "dist",
23
+ "scripts/googleads-report.mjs",
23
24
  "assets",
24
25
  "tool-manifest.json",
25
26
  "LICENSE"
26
27
  ],
27
28
  "scripts": {
29
+ "report:googleads": "node scripts/googleads-report.mjs",
28
30
  "build": "tsc",
29
31
  "dev": "tsx src/index.ts",
30
32
  "auth": "tsx src/auth/cli.ts",
@@ -65,7 +67,7 @@
65
67
  "dependencies": {
66
68
  "@anthropic-ai/sdk": "^0.52.0",
67
69
  "@modelcontextprotocol/sdk": "^1.12.1",
68
- "@onesub/providers": "^0.4.0",
70
+ "@onesub/providers": "^0.4.4",
69
71
  "googleapis": "^171.4.0",
70
72
  "jose": "^5.10.0",
71
73
  "open": "^10.1.0",
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // Read-only entry point using the same implementation and auth as the MCP tools.
3
+ import process from 'node:process';
4
+ import console from 'node:console';
5
+ import { requireAuth } from '../dist/helpers.js';
6
+ import { requireConfig } from '../dist/googleads/config.js';
7
+ import { getCampaignReport, REPORT_METRIC_NOTE } from '../dist/googleads/tools.js';
8
+
9
+ const [startDate, endDate, ...extra] = process.argv.slice(2);
10
+ if (!startDate || !endDate || extra.length) {
11
+ console.error('Usage: node scripts/googleads-report.mjs YYYY-MM-DD YYYY-MM-DD');
12
+ process.exitCode = 1;
13
+ } else {
14
+ try {
15
+ const auth = await requireAuth('https://www.googleapis.com/auth/adwords');
16
+ const config = requireConfig();
17
+ const campaigns = await getCampaignReport(auth, config, { startDate, endDate });
18
+ console.log(JSON.stringify({
19
+ period: { startDate, endDate },
20
+ fetchedAt: new Date().toISOString(),
21
+ customerId: config.customerId,
22
+ currencyCode: campaigns[0]?.currencyCode ?? null,
23
+ timeZone: campaigns[0]?.timeZone ?? null,
24
+ totalCost: campaigns.reduce((sum, row) => sum + row.cost, 0),
25
+ metricNote: REPORT_METRIC_NOTE,
26
+ campaigns,
27
+ }, null, 2));
28
+ } catch (error) {
29
+ console.error(error instanceof Error ? error.message : 'Google Ads report failed');
30
+ process.exitCode = 1;
31
+ }
32
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "등록된 MCP 도구 + 도메인 메타데이터(label·credential·summary)의 SSOT. src/__tests__/tool-manifest.test.ts 가 실제 서버 등록 목록과 diff 하고 메타데이터 정합성을 검사한다. 도구 추가/삭제/개명 시 이 파일을 함께 갱신할 것 — 산문 문서에는 정확한 개수를 쓰지 말고 이 파일을 가리킬 것. mimi-seed://tools/catalog 리소스가 이 파일을 그대로 서빙한다.",
3
- "total": 234,
3
+ "total": 244,
4
4
  "domains": {
5
5
  "admob": {
6
6
  "label": "AdMob",
@@ -382,6 +382,23 @@
382
382
  "video_job_status",
383
383
  "video_validate"
384
384
  ]
385
+ },
386
+ "youtube": {
387
+ "label": "YouTube",
388
+ "credential": "Google OAuth (youtube 또는 youtube_analytics 권한)",
389
+ "summary": "인증 채널·업로드 영상·YouTube Analytics 기간 리포트 읽기",
390
+ "tools": [
391
+ "youtube_get_channel",
392
+ "youtube_list_videos",
393
+ "youtube_get_analytics_report",
394
+ "youtube_update_video_metadata",
395
+ "youtube_set_thumbnail",
396
+ "youtube_schedule_video",
397
+ "youtube_list_comments",
398
+ "youtube_list_comment_replies",
399
+ "youtube_reply_comment",
400
+ "youtube_get_content_insights"
401
+ ]
385
402
  }
386
403
  }
387
404
  }