@yoonion/mimi-seed-mcp 0.3.33 → 0.3.34

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.
@@ -13,6 +13,7 @@ const SCOPES = [
13
13
  'https://www.googleapis.com/auth/admob.monetization',
14
14
  'https://www.googleapis.com/auth/androidpublisher',
15
15
  'https://www.googleapis.com/auth/adwords', // Google Ads API (googleads_* tools)
16
+ 'https://www.googleapis.com/auth/webmasters', // Search Console API (gsc_* tools, 사이트맵 제출 포함)
16
17
  ];
17
18
  // Primary config dir. Legacy `~/.preseed` is read as a fallback during the
18
19
  // rebrand so existing auth sessions don't force a re-login; new writes go to
@@ -0,0 +1,39 @@
1
+ import type { OAuth2Client } from 'google-auth-library';
2
+ export type GscAuth = OAuth2Client;
3
+ export declare function listSites(auth: GscAuth): Promise<import("googleapis").searchconsole_v1.Schema$WmxSite[]>;
4
+ export declare function listSitemaps(auth: GscAuth, siteUrl: string, sitemapIndex?: string): Promise<import("googleapis").searchconsole_v1.Schema$WmxSitemap[]>;
5
+ export declare function getSitemap(auth: GscAuth, siteUrl: string, feedpath: string): Promise<import("googleapis").searchconsole_v1.Schema$WmxSitemap>;
6
+ /** 사이트맵 제출 — webmasters(read-write) 스코프 필요. 응답 본문은 없음(204). */
7
+ export declare function submitSitemap(auth: GscAuth, siteUrl: string, feedpath: string): Promise<{
8
+ submitted: string;
9
+ siteUrl: string;
10
+ }>;
11
+ export declare function inspectUrl(auth: GscAuth, siteUrl: string, inspectionUrl: string, languageCode?: string): Promise<import("googleapis").searchconsole_v1.Schema$UrlInspectionResult>;
12
+ export interface SearchAnalyticsParams {
13
+ startDate: string;
14
+ endDate: string;
15
+ dimensions?: string[];
16
+ rowLimit?: number;
17
+ startRow?: number;
18
+ type?: 'web' | 'image' | 'video' | 'news' | 'discover' | 'googleNews';
19
+ }
20
+ export declare function searchAnalytics(auth: GscAuth, siteUrl: string, params: SearchAnalyticsParams): Promise<import("googleapis").searchconsole_v1.Schema$ApiDataRow[]>;
21
+ export interface SearchAnalyticsRow {
22
+ keys?: string[] | null;
23
+ clicks?: number | null;
24
+ impressions?: number | null;
25
+ ctr?: number | null;
26
+ position?: number | null;
27
+ }
28
+ /**
29
+ * Search Analytics rows 요약.
30
+ * position 은 노출수(impressions) 가중 평균으로 집계한다 — 단순 산술평균은
31
+ * 노출이 1인 행과 1000인 행을 동등 취급해 실제 평균 순위를 왜곡한다.
32
+ */
33
+ export declare function summarizeRows(rows: SearchAnalyticsRow[]): {
34
+ rowCount: number;
35
+ totalClicks: number;
36
+ totalImpressions: number;
37
+ avgCtr: number;
38
+ avgPosition: number;
39
+ };
@@ -0,0 +1,68 @@
1
+ import { google } from 'googleapis';
2
+ /**
3
+ * Google Search Console (Webmasters) API v1 래퍼.
4
+ *
5
+ * `searchconsole('v1')` 은 구 `webmasters('v3')` 의 후속으로, sites/sitemaps/
6
+ * searchanalytics 에 더해 URL Inspection(`urlInspection.index.inspect`)을 포함한다.
7
+ * 인증은 mimi-seed OAuth 클라이언트(`requireAuth()`)를 그대로 사용 — webmasters 스코프 필요.
8
+ */
9
+ const sc = () => google.searchconsole('v1');
10
+ // ─── 속성(사이트) 목록 ───
11
+ export async function listSites(auth) {
12
+ const res = await sc().sites.list({ auth });
13
+ return res.data.siteEntry ?? [];
14
+ }
15
+ // ─── 사이트맵 ───
16
+ export async function listSitemaps(auth, siteUrl, sitemapIndex) {
17
+ const res = await sc().sitemaps.list({ auth, siteUrl, sitemapIndex });
18
+ return res.data.sitemap ?? [];
19
+ }
20
+ export async function getSitemap(auth, siteUrl, feedpath) {
21
+ const res = await sc().sitemaps.get({ auth, siteUrl, feedpath });
22
+ return res.data;
23
+ }
24
+ /** 사이트맵 제출 — webmasters(read-write) 스코프 필요. 응답 본문은 없음(204). */
25
+ export async function submitSitemap(auth, siteUrl, feedpath) {
26
+ await sc().sitemaps.submit({ auth, siteUrl, feedpath });
27
+ return { submitted: feedpath, siteUrl };
28
+ }
29
+ // ─── URL 색인 검사 ───
30
+ export async function inspectUrl(auth, siteUrl, inspectionUrl, languageCode = 'en-US') {
31
+ const res = await sc().urlInspection.index.inspect({
32
+ auth,
33
+ requestBody: { siteUrl, inspectionUrl, languageCode },
34
+ });
35
+ return res.data.inspectionResult ?? {};
36
+ }
37
+ export async function searchAnalytics(auth, siteUrl, params) {
38
+ const res = await sc().searchanalytics.query({
39
+ auth,
40
+ siteUrl,
41
+ requestBody: {
42
+ startDate: params.startDate,
43
+ endDate: params.endDate,
44
+ dimensions: params.dimensions,
45
+ rowLimit: params.rowLimit ?? 1000,
46
+ startRow: params.startRow,
47
+ type: params.type,
48
+ },
49
+ });
50
+ return res.data.rows ?? [];
51
+ }
52
+ /**
53
+ * Search Analytics rows 요약.
54
+ * position 은 노출수(impressions) 가중 평균으로 집계한다 — 단순 산술평균은
55
+ * 노출이 1인 행과 1000인 행을 동등 취급해 실제 평균 순위를 왜곡한다.
56
+ */
57
+ export function summarizeRows(rows) {
58
+ const totalClicks = rows.reduce((s, r) => s + (r.clicks ?? 0), 0);
59
+ const totalImpressions = rows.reduce((s, r) => s + (r.impressions ?? 0), 0);
60
+ const weightedPos = rows.reduce((s, r) => s + (r.position ?? 0) * (r.impressions ?? 0), 0);
61
+ return {
62
+ rowCount: rows.length,
63
+ totalClicks,
64
+ totalImpressions,
65
+ avgCtr: totalImpressions > 0 ? Math.round((totalClicks / totalImpressions) * 10000) / 10000 : 0,
66
+ avgPosition: totalImpressions > 0 ? Math.round((weightedPos / totalImpressions) * 100) / 100 : 0,
67
+ };
68
+ }
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { registerCiTools } from './registers/ci.js';
15
15
  import { registerInstagramTools } from './registers/instagram.js';
16
16
  import { registerFacebookTools } from './registers/facebook.js';
17
17
  import { registerGoogleAdsTools } from './registers/googleads.js';
18
+ import { registerGscTools } from './registers/gsc.js';
18
19
  import { registerPrompts } from './prompts.js';
19
20
  import { registerResources } from './resources.js';
20
21
  // dist/index.js 기준 ../package.json — npm 패키지 루트의 버전을 단일 출처로 사용.
@@ -37,6 +38,7 @@ registerCiTools(server);
37
38
  registerInstagramTools(server);
38
39
  registerFacebookTools(server);
39
40
  registerGoogleAdsTools(server);
41
+ registerGscTools(server);
40
42
  registerPrompts(server);
41
43
  registerResources(server);
42
44
  // `npx -y @yoonion/mimi-seed-mcp <subcommand>` 처리.
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerGscTools(server: McpServer): void;
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { requireAuth } from '../helpers.js';
3
+ import * as gsc from '../gsc/tools.js';
4
+ const SITE_URL_DESC = "Search Console 속성 식별자. 도메인 속성은 'sc-domain:example.com', URL 접두어 속성은 'https://example.com/' 형식.";
5
+ export function registerGscTools(server) {
6
+ server.tool('gsc_list_sites', 'Search Console에 등록된(권한 있는) 속성 목록 + 권한 레벨 조회. siteUrl 값을 확인할 때 먼저 호출.', {}, async () => {
7
+ const auth = await requireAuth();
8
+ const sites = await gsc.listSites(auth);
9
+ return { content: [{ type: 'text', text: JSON.stringify(sites, null, 2) }] };
10
+ });
11
+ server.tool('gsc_list_sitemaps', '속성에 제출된 사이트맵 목록 (마지막 다운로드 시각, 경고/오류 수, submitted/indexed 카운트). ⚠️ 사이트맵 리포트의 indexed 수치는 신뢰도 낮음 — 실제 색인 여부는 gsc_inspect_url 로 확인.', {
12
+ siteUrl: z.string().describe(SITE_URL_DESC),
13
+ sitemapIndex: z.string().optional().describe('사이트맵 인덱스 URL — 인덱스 하위 사이트맵만 보고 싶을 때'),
14
+ }, async ({ siteUrl, sitemapIndex }) => {
15
+ const auth = await requireAuth();
16
+ const sitemaps = await gsc.listSitemaps(auth, siteUrl, sitemapIndex);
17
+ return { content: [{ type: 'text', text: JSON.stringify(sitemaps, null, 2) }] };
18
+ });
19
+ server.tool('gsc_get_sitemap', '특정 사이트맵 1개의 상세(타입별 submitted/indexed, 경고/오류, 처리 시각) 조회.', {
20
+ siteUrl: z.string().describe(SITE_URL_DESC),
21
+ feedpath: z.string().describe('사이트맵 전체 URL (예: https://example.com/sitemap.xml)'),
22
+ }, async ({ siteUrl, feedpath }) => {
23
+ const auth = await requireAuth();
24
+ const sitemap = await gsc.getSitemap(auth, siteUrl, feedpath);
25
+ return { content: [{ type: 'text', text: JSON.stringify(sitemap, null, 2) }] };
26
+ });
27
+ server.tool('gsc_submit_sitemap', '사이트맵을 Search Console에 제출(또는 재제출)해 재크롤을 유도. webmasters read-write 스코프 필요.', {
28
+ siteUrl: z.string().describe(SITE_URL_DESC),
29
+ feedpath: z.string().describe('제출할 사이트맵 전체 URL (예: https://example.com/sitemap.xml)'),
30
+ }, async ({ siteUrl, feedpath }) => {
31
+ const auth = await requireAuth();
32
+ const result = await gsc.submitSitemap(auth, siteUrl, feedpath);
33
+ return {
34
+ content: [{
35
+ type: 'text',
36
+ text: `✅ 사이트맵 제출 완료.\n ${result.submitted}\n\n반영까지 수 분~수 시간 걸려. gsc_list_sitemaps 로 lastDownloaded 갱신을 확인해.`,
37
+ }],
38
+ };
39
+ });
40
+ server.tool('gsc_inspect_url', 'URL 1개의 실제 색인 상태 검사 (coverageState, robots.txt 허용 여부, 마지막 크롤 시각, 정규 URL, 리치 결과). 사이트맵 리포트보다 정확한 색인 진단의 단일 출처.', {
41
+ siteUrl: z.string().describe(SITE_URL_DESC),
42
+ inspectionUrl: z.string().describe("검사할 전체 URL — 반드시 siteUrl 속성 하위여야 함 (예: 'https://example.com/page')"),
43
+ languageCode: z.string().optional().describe("이슈 메시지 언어 (BCP-47, 예: 'ko', 'en-US'). 기본 en-US"),
44
+ }, async ({ siteUrl, inspectionUrl, languageCode }) => {
45
+ const auth = await requireAuth();
46
+ const result = await gsc.inspectUrl(auth, siteUrl, inspectionUrl, languageCode ?? 'en-US');
47
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
48
+ });
49
+ server.tool('gsc_search_analytics', '검색 성과 데이터 조회 (클릭·노출·CTR·평균순위). dimensions로 query/page/country/device/date/searchAppearance 별 분해. 요약(가중 평균순위 포함) + rows 반환.', {
50
+ siteUrl: z.string().describe(SITE_URL_DESC),
51
+ startDate: z.string().describe('시작일 (YYYY-MM-DD)'),
52
+ endDate: z.string().describe('종료일 (YYYY-MM-DD)'),
53
+ dimensions: z.string().optional().describe('쉼표 구분 분해 차원: query,page,country,device,date,searchAppearance (생략 시 전체 합계)'),
54
+ rowLimit: z.number().int().min(1).max(25000).optional().describe('최대 행 수 (기본 1000, 최대 25000)'),
55
+ type: z.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews']).optional().describe('검색 유형 필터 (기본 web)'),
56
+ }, async ({ siteUrl, startDate, endDate, dimensions, rowLimit, type }) => {
57
+ const auth = await requireAuth();
58
+ const dims = dimensions
59
+ ? dimensions.split(',').map((d) => d.trim()).filter(Boolean)
60
+ : undefined;
61
+ const rows = await gsc.searchAnalytics(auth, siteUrl, {
62
+ startDate,
63
+ endDate,
64
+ dimensions: dims,
65
+ rowLimit,
66
+ type,
67
+ });
68
+ return {
69
+ content: [{
70
+ type: 'text',
71
+ text: JSON.stringify({
72
+ period: { startDate, endDate },
73
+ dimensions: dims ?? [],
74
+ summary: gsc.summarizeRows(rows),
75
+ rows,
76
+ }, null, 2),
77
+ }],
78
+ };
79
+ });
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.33",
3
+ "version": "0.3.34",
4
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": {