@yoonion/mimi-seed-mcp 0.3.23 → 0.3.24

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.
@@ -1,4 +1,5 @@
1
1
  import type { InstagramConfig } from './config.js';
2
+ export declare function apiBaseFor(token: string): string;
2
3
  export interface InstagramAccount {
3
4
  id: string;
4
5
  username: string;
@@ -9,6 +10,7 @@ export interface InstagramAccount {
9
10
  media_count?: number;
10
11
  }
11
12
  export declare function getAccount(cfg: InstagramConfig): Promise<InstagramAccount>;
13
+ export declare function fetchUserId(accessToken: string): Promise<string>;
12
14
  export interface PublishResult {
13
15
  id: string;
14
16
  permalink?: string;
@@ -1,6 +1,14 @@
1
- const BASE = 'https://graph.facebook.com/v21.0';
2
- async function igFetch(endpoint, params, method = 'GET') {
3
- const url = new URL(`${BASE}${endpoint}`);
1
+ // 가지 Meta API:
2
+ // IGAA... = Instagram API with Instagram Login (2024 신규) — graph.instagram.com
3
+ // EAA... = Instagram Graph API via Facebook Login — graph.facebook.com
4
+ // 토큰 prefix로 자동 분기. 엔드포인트 path는 둘 다 동일.
5
+ export function apiBaseFor(token) {
6
+ if (token.startsWith('IGAA'))
7
+ return 'https://graph.instagram.com/v21.0';
8
+ return 'https://graph.facebook.com/v21.0';
9
+ }
10
+ async function igFetch(token, endpoint, params, method = 'GET') {
11
+ const url = new URL(`${apiBaseFor(token)}${endpoint}`);
4
12
  let body;
5
13
  if (method === 'GET') {
6
14
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
@@ -28,17 +36,36 @@ async function igFetch(endpoint, params, method = 'GET') {
28
36
  return JSON.parse(text);
29
37
  }
30
38
  export async function getAccount(cfg) {
31
- return igFetch(`/${cfg.userId}`, {
32
- fields: 'id,username,name,profile_picture_url,account_type,followers_count,media_count',
39
+ // graph.instagram.com 은 name/profile_picture_url 필드를 받지 않을 수 있어
40
+ // 두 API 공통으로 안전한 필드만 요청
41
+ const fields = cfg.accessToken.startsWith('IGAA')
42
+ ? 'id,username,account_type,followers_count,media_count'
43
+ : 'id,username,name,profile_picture_url,account_type,followers_count,media_count';
44
+ return igFetch(cfg.accessToken, `/${cfg.userId}`, {
45
+ fields,
33
46
  access_token: cfg.accessToken,
34
47
  });
35
48
  }
49
+ // 토큰만으로 user ID 자동 조회 (양쪽 API 지원)
50
+ export async function fetchUserId(accessToken) {
51
+ if (accessToken.startsWith('IGAA')) {
52
+ // Instagram Login: GET /me?fields=user_id,username
53
+ const res = await igFetch(accessToken, '/me', { fields: 'user_id,username', access_token: accessToken });
54
+ // graph.instagram.com 은 v21.0에서 user_id 또는 id 둘 중 하나 반환
55
+ return res.user_id ?? res.id;
56
+ }
57
+ // Facebook Login: GET /me/accounts → instagram_business_account.id
58
+ const res = await igFetch(accessToken, '/me/accounts', { fields: 'instagram_business_account', access_token: accessToken });
59
+ const id = res.data?.[0]?.instagram_business_account?.id;
60
+ if (!id) {
61
+ throw new Error('Facebook Page에 연결된 Instagram Business Account를 찾지 못했습니다.\n' +
62
+ 'Facebook Page → Settings → Linked Accounts에서 IG 계정을 연결하세요.');
63
+ }
64
+ return id;
65
+ }
36
66
  async function fetchPermalink(cfg, mediaId) {
37
67
  try {
38
- const meta = await igFetch(`/${mediaId}`, {
39
- fields: 'permalink',
40
- access_token: cfg.accessToken,
41
- });
68
+ const meta = await igFetch(cfg.accessToken, `/${mediaId}`, { fields: 'permalink', access_token: cfg.accessToken });
42
69
  return meta.permalink;
43
70
  }
44
71
  catch {
@@ -47,9 +74,9 @@ async function fetchPermalink(cfg, mediaId) {
47
74
  }
48
75
  export async function postImage(cfg, imageUrl, caption) {
49
76
  // Step 1: image container 생성
50
- const container = await igFetch(`/${cfg.userId}/media`, { image_url: imageUrl, caption, access_token: cfg.accessToken }, 'POST');
77
+ const container = await igFetch(cfg.accessToken, `/${cfg.userId}/media`, { image_url: imageUrl, caption, access_token: cfg.accessToken }, 'POST');
51
78
  // Step 2: publish
52
- const published = await igFetch(`/${cfg.userId}/media_publish`, { creation_id: container.id, access_token: cfg.accessToken }, 'POST');
79
+ const published = await igFetch(cfg.accessToken, `/${cfg.userId}/media_publish`, { creation_id: container.id, access_token: cfg.accessToken }, 'POST');
53
80
  return {
54
81
  id: published.id,
55
82
  permalink: await fetchPermalink(cfg, published.id),
@@ -62,18 +89,18 @@ export async function postCarousel(cfg, imageUrls, caption) {
62
89
  // Step 1: 각 이미지를 children container로 생성 (sequential — 일부 실패 시 부분 결과 명확)
63
90
  const childIds = [];
64
91
  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');
92
+ const child = await igFetch(cfg.accessToken, `/${cfg.userId}/media`, { image_url: url, is_carousel_item: 'true', access_token: cfg.accessToken }, 'POST');
66
93
  childIds.push(child.id);
67
94
  }
68
95
  // Step 2: carousel container 생성
69
- const carousel = await igFetch(`/${cfg.userId}/media`, {
96
+ const carousel = await igFetch(cfg.accessToken, `/${cfg.userId}/media`, {
70
97
  media_type: 'CAROUSEL',
71
98
  children: childIds.join(','),
72
99
  caption,
73
100
  access_token: cfg.accessToken,
74
101
  }, 'POST');
75
102
  // Step 3: publish
76
- const published = await igFetch(`/${cfg.userId}/media_publish`, { creation_id: carousel.id, access_token: cfg.accessToken }, 'POST');
103
+ const published = await igFetch(cfg.accessToken, `/${cfg.userId}/media_publish`, { creation_id: carousel.id, access_token: cfg.accessToken }, 'POST');
77
104
  return {
78
105
  id: published.id,
79
106
  permalink: await fetchPermalink(cfg, published.id),
@@ -3,49 +3,77 @@ import { requireInstagramConfig, saveInstagramConfig } from '../instagram/config
3
3
  import * as api from '../instagram/api.js';
4
4
  export function registerInstagramTools(server) {
5
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 토큰 검증 권장.',
6
+ 'Instagram 토큰을 ~/.mimi-seed/instagram.json (mode 0600)에 저장합니다.',
7
+ 'accessToken 형식 모두 자동 감지:',
8
+ ' IGAA... = Instagram API with Instagram Login (Meta 신규, FB Page 불필요)',
9
+ ' EAA... = Instagram Graph API via Facebook Login (FB Page+IG Business 연결 필요)',
10
+ 'userId 미입력 토큰으로 자동 조회 (/me 또는 /me/accounts).',
11
+ '저장 직후 토큰 유효성도 자동 검증.',
11
12
  ].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일 자동 계산'),
13
+ accessToken: z.string().describe('Long-lived access token (60일)'),
14
+ userId: z.string().optional().describe('Instagram Business Account ID (생략 시 자동 조회)'),
15
+ assumeIssuedNow: z.boolean().default(true).describe('expiresAt = 지금 + 60일 자동 계산'),
15
16
  }, async ({ accessToken, userId, assumeIssuedNow }) => {
17
+ const apiType = accessToken.startsWith('IGAA') ? 'Instagram Login' : 'Facebook Login';
18
+ // userId 자동 조회
19
+ let resolvedUserId = userId;
20
+ if (!resolvedUserId) {
21
+ try {
22
+ resolvedUserId = await api.fetchUserId(accessToken);
23
+ }
24
+ catch (err) {
25
+ return {
26
+ content: [{
27
+ type: 'text',
28
+ text: [
29
+ `❌ userId 자동 조회 실패 (${apiType})`,
30
+ ` ${err.message}`,
31
+ ``,
32
+ `userId를 명시적으로 전달하거나 토큰을 다시 확인하세요.`,
33
+ ].join('\n'),
34
+ }],
35
+ };
36
+ }
37
+ }
16
38
  const expiresAt = assumeIssuedNow
17
39
  ? new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString()
18
40
  : undefined;
19
- saveInstagramConfig({ accessToken, userId, expiresAt });
20
41
  // 검증 — 잘못된 토큰이면 즉시 알림
21
42
  try {
22
- const account = await api.getAccount({ accessToken, userId });
23
- // 검증 성공 username 추가 저장
24
- saveInstagramConfig({ accessToken, userId, expiresAt, username: account.username });
43
+ const account = await api.getAccount({ accessToken, userId: resolvedUserId });
44
+ // 검증 성공 최종 저장
45
+ saveInstagramConfig({
46
+ accessToken,
47
+ userId: resolvedUserId,
48
+ expiresAt,
49
+ username: account.username,
50
+ });
25
51
  return {
26
52
  content: [{
27
53
  type: 'text',
28
54
  text: [
29
- `✅ Instagram 연결 확인 완료`,
55
+ `✅ Instagram 연결 확인 완료 (${apiType})`,
30
56
  ` 계정: @${account.username}${account.name ? ` (${account.name})` : ''}`,
31
57
  ` ID: ${account.id}`,
32
58
  account.account_type ? ` 타입: ${account.account_type}` : '',
33
59
  account.followers_count !== undefined ? ` 팔로워: ${account.followers_count.toLocaleString()}` : '',
60
+ account.media_count !== undefined ? ` 게시물: ${account.media_count}` : '',
34
61
  expiresAt ? ` 토큰 만료(추정): ${expiresAt.slice(0, 10)}` : '',
35
62
  ].filter(Boolean).join('\n'),
36
63
  }],
37
64
  };
38
65
  }
39
66
  catch (err) {
67
+ // 검증 실패 — 저장은 하지 않고 사용자에게 알림
40
68
  return {
41
69
  content: [{
42
70
  type: 'text',
43
71
  text: [
44
- `⚠️ 설정은 저장됐지만 토큰 검증 실패`,
72
+ `❌ 토큰 검증 실패 (${apiType})`,
73
+ ` userId: ${resolvedUserId}`,
45
74
  ` ${err.message}`,
46
75
  ``,
47
- `토큰/userId를 다시 확인하세요.`,
48
- `instagram_save_config 로 재저장 가능.`,
76
+ `토큰을 다시 발급받거나 userId를 직접 지정해보세요.`,
49
77
  ].join('\n'),
50
78
  }],
51
79
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
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": {