@yoonion/mimi-seed-mcp 0.3.22 → 0.3.23

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/index.js CHANGED
@@ -11,6 +11,7 @@ import { registerAiTools } from './registers/ai.js';
11
11
  import { registerBigqueryTools } from './registers/bigquery.js';
12
12
  import { registerAuthTools } from './registers/auth.js';
13
13
  import { registerCiTools } from './registers/ci.js';
14
+ import { registerInstagramTools } from './registers/instagram.js';
14
15
  import { registerPrompts } from './prompts.js';
15
16
  import { registerResources } from './resources.js';
16
17
  const server = new McpServer({
@@ -27,6 +28,7 @@ registerAiTools(server);
27
28
  registerBigqueryTools(server);
28
29
  registerAuthTools(server);
29
30
  registerCiTools(server);
31
+ registerInstagramTools(server);
30
32
  registerPrompts(server);
31
33
  registerResources(server);
32
34
  // `npx -y @yoonion/mimi-seed-mcp <subcommand>` 처리.
@@ -0,0 +1,17 @@
1
+ import type { InstagramConfig } from './config.js';
2
+ export interface InstagramAccount {
3
+ id: string;
4
+ username: string;
5
+ name?: string;
6
+ profile_picture_url?: string;
7
+ account_type?: string;
8
+ followers_count?: number;
9
+ media_count?: number;
10
+ }
11
+ export declare function getAccount(cfg: InstagramConfig): Promise<InstagramAccount>;
12
+ export interface PublishResult {
13
+ id: string;
14
+ permalink?: string;
15
+ }
16
+ export declare function postImage(cfg: InstagramConfig, imageUrl: string, caption: string): Promise<PublishResult>;
17
+ export declare function postCarousel(cfg: InstagramConfig, imageUrls: string[], caption: string): Promise<PublishResult>;
@@ -0,0 +1,81 @@
1
+ const BASE = 'https://graph.facebook.com/v21.0';
2
+ async function igFetch(endpoint, params, method = 'GET') {
3
+ const url = new URL(`${BASE}${endpoint}`);
4
+ let body;
5
+ if (method === 'GET') {
6
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
7
+ }
8
+ else {
9
+ body = new URLSearchParams(params).toString();
10
+ }
11
+ const res = await fetch(url.toString(), {
12
+ method,
13
+ body,
14
+ headers: method === 'POST' ? { 'Content-Type': 'application/x-www-form-urlencoded' } : undefined,
15
+ });
16
+ const text = await res.text();
17
+ if (!res.ok) {
18
+ let msg = text;
19
+ try {
20
+ const parsed = JSON.parse(text);
21
+ if (parsed.error) {
22
+ msg = `${parsed.error.message} (code ${parsed.error.code}${parsed.error.error_subcode ? `/${parsed.error.error_subcode}` : ''})`;
23
+ }
24
+ }
25
+ catch { /* fall through with raw text */ }
26
+ throw new Error(`Instagram API ${res.status}: ${msg}`);
27
+ }
28
+ return JSON.parse(text);
29
+ }
30
+ export async function getAccount(cfg) {
31
+ return igFetch(`/${cfg.userId}`, {
32
+ fields: 'id,username,name,profile_picture_url,account_type,followers_count,media_count',
33
+ access_token: cfg.accessToken,
34
+ });
35
+ }
36
+ async function fetchPermalink(cfg, mediaId) {
37
+ try {
38
+ const meta = await igFetch(`/${mediaId}`, {
39
+ fields: 'permalink',
40
+ access_token: cfg.accessToken,
41
+ });
42
+ return meta.permalink;
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ export async function postImage(cfg, imageUrl, caption) {
49
+ // Step 1: image container 생성
50
+ const container = await igFetch(`/${cfg.userId}/media`, { image_url: imageUrl, caption, access_token: cfg.accessToken }, 'POST');
51
+ // Step 2: publish
52
+ const published = await igFetch(`/${cfg.userId}/media_publish`, { creation_id: container.id, access_token: cfg.accessToken }, 'POST');
53
+ return {
54
+ id: published.id,
55
+ permalink: await fetchPermalink(cfg, published.id),
56
+ };
57
+ }
58
+ export async function postCarousel(cfg, imageUrls, caption) {
59
+ if (imageUrls.length < 2 || imageUrls.length > 10) {
60
+ throw new Error(`캐러셀은 2~10장의 이미지가 필요합니다 (받은 수: ${imageUrls.length}).`);
61
+ }
62
+ // Step 1: 각 이미지를 children container로 생성 (sequential — 일부 실패 시 부분 결과 명확)
63
+ const childIds = [];
64
+ for (const url of imageUrls) {
65
+ const child = await igFetch(`/${cfg.userId}/media`, { image_url: url, is_carousel_item: 'true', access_token: cfg.accessToken }, 'POST');
66
+ childIds.push(child.id);
67
+ }
68
+ // Step 2: carousel container 생성
69
+ const carousel = await igFetch(`/${cfg.userId}/media`, {
70
+ media_type: 'CAROUSEL',
71
+ children: childIds.join(','),
72
+ caption,
73
+ access_token: cfg.accessToken,
74
+ }, 'POST');
75
+ // Step 3: publish
76
+ const published = await igFetch(`/${cfg.userId}/media_publish`, { creation_id: carousel.id, access_token: cfg.accessToken }, 'POST');
77
+ return {
78
+ id: published.id,
79
+ permalink: await fetchPermalink(cfg, published.id),
80
+ };
81
+ }
@@ -0,0 +1,9 @@
1
+ export interface InstagramConfig {
2
+ accessToken: string;
3
+ userId: string;
4
+ expiresAt?: string;
5
+ username?: string;
6
+ }
7
+ export declare function loadInstagramConfig(): InstagramConfig | null;
8
+ export declare function saveInstagramConfig(cfg: InstagramConfig): void;
9
+ export declare function requireInstagramConfig(): InstagramConfig;
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const CONFIG_PATH = path.join(os.homedir(), '.mimi-seed', 'instagram.json');
5
+ export function loadInstagramConfig() {
6
+ try {
7
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
13
+ export function saveInstagramConfig(cfg) {
14
+ const dir = path.dirname(CONFIG_PATH);
15
+ if (!fs.existsSync(dir)) {
16
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
17
+ }
18
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
19
+ if (process.platform !== 'win32') {
20
+ fs.chmodSync(CONFIG_PATH, 0o600);
21
+ }
22
+ }
23
+ export function requireInstagramConfig() {
24
+ const cfg = loadInstagramConfig();
25
+ if (!cfg) {
26
+ throw new Error('Instagram 설정이 없습니다.\n' +
27
+ 'instagram_save_config 도구로 먼저 설정해주세요.\n' +
28
+ '예: instagram_save_config(accessToken="...", userId="...")');
29
+ }
30
+ return cfg;
31
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerInstagramTools(server: McpServer): void;
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod';
2
+ import { requireInstagramConfig, saveInstagramConfig } from '../instagram/config.js';
3
+ import * as api from '../instagram/api.js';
4
+ export function registerInstagramTools(server) {
5
+ server.tool('instagram_save_config', [
6
+ 'Instagram Graph API 토큰을 ~/.mimi-seed/instagram.json (mode 0600)에 저장합니다.',
7
+ 'accessToken: long-lived (60일) — developers.facebook.com → Graph API Explorer에서 발급.',
8
+ 'userId: Instagram Business Account ID — GET /me/accounts → instagram_business_account.id 로 조회.',
9
+ '권한: instagram_basic + instagram_content_publish 필수.',
10
+ '저장 후 instagram_get_account 로 토큰 검증 권장.',
11
+ ].join(' '), {
12
+ accessToken: z.string().describe('Long-lived access token (60일 유효)'),
13
+ userId: z.string().describe('Instagram Business Account ID'),
14
+ assumeIssuedNow: z.boolean().default(true).describe('issuedAt을 지금으로 가정하고 expiresAt = +60일 자동 계산'),
15
+ }, async ({ accessToken, userId, assumeIssuedNow }) => {
16
+ const expiresAt = assumeIssuedNow
17
+ ? new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString()
18
+ : undefined;
19
+ saveInstagramConfig({ accessToken, userId, expiresAt });
20
+ // 검증 — 잘못된 토큰이면 즉시 알림
21
+ try {
22
+ const account = await api.getAccount({ accessToken, userId });
23
+ // 검증 성공 시 username 추가 저장
24
+ saveInstagramConfig({ accessToken, userId, expiresAt, username: account.username });
25
+ return {
26
+ content: [{
27
+ type: 'text',
28
+ text: [
29
+ `✅ Instagram 연결 확인 완료`,
30
+ ` 계정: @${account.username}${account.name ? ` (${account.name})` : ''}`,
31
+ ` ID: ${account.id}`,
32
+ account.account_type ? ` 타입: ${account.account_type}` : '',
33
+ account.followers_count !== undefined ? ` 팔로워: ${account.followers_count.toLocaleString()}` : '',
34
+ expiresAt ? ` 토큰 만료(추정): ${expiresAt.slice(0, 10)}` : '',
35
+ ].filter(Boolean).join('\n'),
36
+ }],
37
+ };
38
+ }
39
+ catch (err) {
40
+ return {
41
+ content: [{
42
+ type: 'text',
43
+ text: [
44
+ `⚠️ 설정은 저장됐지만 토큰 검증 실패`,
45
+ ` ${err.message}`,
46
+ ``,
47
+ `토큰/userId를 다시 확인하세요.`,
48
+ `instagram_save_config 로 재저장 가능.`,
49
+ ].join('\n'),
50
+ }],
51
+ };
52
+ }
53
+ });
54
+ server.tool('instagram_get_account', 'Instagram 계정 정보 조회 + 저장된 토큰 유효성 검증.', {}, async () => {
55
+ const cfg = requireInstagramConfig();
56
+ const account = await api.getAccount(cfg);
57
+ const remainingDays = cfg.expiresAt
58
+ ? Math.round((new Date(cfg.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000))
59
+ : null;
60
+ return {
61
+ content: [{
62
+ type: 'text',
63
+ text: [
64
+ `@${account.username}${account.name ? ` (${account.name})` : ''}`,
65
+ ` ID: ${account.id}`,
66
+ account.account_type ? ` 타입: ${account.account_type}` : '',
67
+ account.followers_count !== undefined ? ` 팔로워: ${account.followers_count.toLocaleString()}` : '',
68
+ account.media_count !== undefined ? ` 게시물: ${account.media_count}` : '',
69
+ remainingDays !== null
70
+ ? remainingDays > 7
71
+ ? ` 토큰: ${remainingDays}일 남음`
72
+ : remainingDays > 0
73
+ ? ` ⚠️ 토큰 ${remainingDays}일 남음 — 곧 갱신 필요`
74
+ : ` ❌ 토큰 만료됨 — instagram_save_config로 재저장`
75
+ : '',
76
+ ].filter(Boolean).join('\n'),
77
+ }],
78
+ };
79
+ });
80
+ server.tool('instagram_post_image', [
81
+ '단일 이미지를 Instagram에 게시합니다.',
82
+ 'imageUrl은 public URL이어야 합니다 (Graph API 제약). 로컬 파일은 미리 S3/R2/Cloudinary 등에 업로드 필요.',
83
+ '캡션에는 해시태그(#)와 멘션(@) 포함 가능. 줄바꿈 \\n 사용.',
84
+ '2-step API: container 생성 → media_publish.',
85
+ ].join(' '), {
86
+ imageUrl: z.string().url().describe('이미지의 public URL (HTTPS 권장)'),
87
+ caption: z.string().describe('캡션 — 해시태그/멘션/줄바꿈 포함 가능'),
88
+ }, async ({ imageUrl, caption }) => {
89
+ const cfg = requireInstagramConfig();
90
+ const result = await api.postImage(cfg, imageUrl, caption);
91
+ return {
92
+ content: [{
93
+ type: 'text',
94
+ text: [
95
+ `✅ 게시 완료`,
96
+ ` media_id: ${result.id}`,
97
+ result.permalink ? ` URL: ${result.permalink}` : '',
98
+ ].filter(Boolean).join('\n'),
99
+ }],
100
+ };
101
+ });
102
+ server.tool('instagram_post_carousel', [
103
+ '여러 이미지를 캐러셀(swipe)로 게시합니다. 2~10장.',
104
+ '모든 이미지는 public URL이어야 함.',
105
+ '3-step API: 각 이미지 children container → carousel container → publish.',
106
+ '일부 이미지에서 실패하면 전체 실패 (이미 만든 children container는 자동 삭제 안 됨 — Instagram이 알아서 처리).',
107
+ ].join(' '), {
108
+ imageUrls: z.array(z.string().url()).min(2).max(10).describe('이미지 URL 배열 (2~10장)'),
109
+ caption: z.string().describe('캡션'),
110
+ }, async ({ imageUrls, caption }) => {
111
+ const cfg = requireInstagramConfig();
112
+ const result = await api.postCarousel(cfg, imageUrls, caption);
113
+ return {
114
+ content: [{
115
+ type: 'text',
116
+ text: [
117
+ `✅ 캐러셀 ${imageUrls.length}장 게시 완료`,
118
+ ` media_id: ${result.id}`,
119
+ result.permalink ? ` URL: ${result.permalink}` : '',
120
+ ].filter(Boolean).join('\n'),
121
+ }],
122
+ };
123
+ });
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.22",
3
+ "version": "0.3.23",
4
4
  "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {