@yoonion/mimi-seed-mcp 0.3.19 → 0.3.21

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.
@@ -0,0 +1,22 @@
1
+ export type CiProvider = 'github' | 'gitlab';
2
+ export interface CiConfig {
3
+ provider: CiProvider;
4
+ token: string;
5
+ owner: string;
6
+ repo: string;
7
+ host?: string;
8
+ }
9
+ export declare function loadCiConfig(): CiConfig | null;
10
+ export declare function saveCiConfig(config: CiConfig): void;
11
+ export declare function requireCiConfig(): CiConfig;
12
+ export interface NormalizedBuild {
13
+ id: number | string;
14
+ name?: string;
15
+ workflow?: string;
16
+ status: string;
17
+ branch: string;
18
+ commit?: string;
19
+ url: string;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ }
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
5
+ const CI_CONFIG_PATH = path.join(CONFIG_DIR, 'ci.json');
6
+ export function loadCiConfig() {
7
+ try {
8
+ return JSON.parse(fs.readFileSync(CI_CONFIG_PATH, 'utf-8'));
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function saveCiConfig(config) {
15
+ if (!fs.existsSync(CONFIG_DIR)) {
16
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
17
+ }
18
+ fs.writeFileSync(CI_CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
19
+ }
20
+ export function requireCiConfig() {
21
+ const cfg = loadCiConfig();
22
+ if (!cfg) {
23
+ throw new Error('CI 설정이 없습니다.\n' +
24
+ 'ci_save_config 도구로 먼저 설정해주세요.\n' +
25
+ '예시:\n' +
26
+ ' GitHub: ci_save_config(provider="github", token="ghp_...", owner="my-org", repo="my-app")\n' +
27
+ ' GitLab: ci_save_config(provider="gitlab", token="glpat-...", owner="my-group", repo="my-app")');
28
+ }
29
+ return cfg;
30
+ }
@@ -0,0 +1,12 @@
1
+ import type { CiConfig, NormalizedBuild } from './config.js';
2
+ export declare function listWorkflows(cfg: CiConfig): Promise<{
3
+ id: any;
4
+ name: any;
5
+ file: string;
6
+ state: any;
7
+ url: any;
8
+ }[]>;
9
+ export declare function triggerBuild(cfg: CiConfig, workflow: string, ref?: string, inputs?: Record<string, string>): Promise<NormalizedBuild | null>;
10
+ export declare function getBuildStatus(cfg: CiConfig, runId: string | number): Promise<NormalizedBuild>;
11
+ export declare function listRecentBuilds(cfg: CiConfig, workflow?: string, limit?: number): Promise<NormalizedBuild[]>;
12
+ export declare function cancelBuild(cfg: CiConfig, runId: string | number): Promise<void>;
@@ -0,0 +1,92 @@
1
+ function base(cfg) {
2
+ // GitHub Enterprise: host = https://github.example.com → API base = https://github.example.com/api/v3
3
+ if (cfg.host)
4
+ return `${cfg.host.replace(/\/$/, '')}/api/v3`;
5
+ return 'https://api.github.com';
6
+ }
7
+ function headers(token) {
8
+ return {
9
+ Authorization: `Bearer ${token}`,
10
+ Accept: 'application/vnd.github+json',
11
+ 'X-GitHub-Api-Version': '2022-11-28',
12
+ 'Content-Type': 'application/json',
13
+ };
14
+ }
15
+ async function ghFetch(cfg, endpoint, options) {
16
+ const res = await fetch(`${base(cfg)}${endpoint}`, {
17
+ ...options,
18
+ headers: { ...headers(cfg.token), ...(options?.headers ?? {}) },
19
+ });
20
+ if (res.status === 204)
21
+ return null;
22
+ const body = await res.text();
23
+ if (!res.ok)
24
+ throw new Error(`GitHub API ${res.status}: ${body}`);
25
+ return JSON.parse(body);
26
+ }
27
+ export async function listWorkflows(cfg) {
28
+ const data = await ghFetch(cfg, `/repos/${cfg.owner}/${cfg.repo}/actions/workflows`);
29
+ return data.workflows.map((w) => ({
30
+ id: w.id,
31
+ name: w.name,
32
+ file: w.path.replace('.github/workflows/', ''),
33
+ state: w.state,
34
+ url: w.html_url,
35
+ }));
36
+ }
37
+ export async function triggerBuild(cfg, workflow, ref = 'main', inputs = {}) {
38
+ const wfId = /^\d+$/.test(workflow) ? Number(workflow) : workflow;
39
+ const startTime = new Date();
40
+ await ghFetch(cfg, `/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/dispatches`, {
41
+ method: 'POST',
42
+ body: JSON.stringify({ ref, inputs }),
43
+ });
44
+ // Dispatch returns 204 with no run ID — poll briefly then filter by start time
45
+ await new Promise((r) => setTimeout(r, 3000));
46
+ const runs = await listRecentBuilds(cfg, workflow, 5);
47
+ return runs.find((r) => new Date(r.createdAt) >= startTime) ?? runs[0] ?? null;
48
+ }
49
+ export async function getBuildStatus(cfg, runId) {
50
+ const data = await ghFetch(cfg, `/repos/${cfg.owner}/${cfg.repo}/actions/runs/${runId}`);
51
+ return normalize(data);
52
+ }
53
+ export async function listRecentBuilds(cfg, workflow, limit = 10) {
54
+ let endpoint;
55
+ if (workflow) {
56
+ const wfId = /^\d+$/.test(workflow) ? Number(workflow) : workflow;
57
+ endpoint = `/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/runs?per_page=${limit}`;
58
+ }
59
+ else {
60
+ endpoint = `/repos/${cfg.owner}/${cfg.repo}/actions/runs?per_page=${limit}`;
61
+ }
62
+ const data = await ghFetch(cfg, endpoint);
63
+ return data.workflow_runs.map(normalize);
64
+ }
65
+ export async function cancelBuild(cfg, runId) {
66
+ await ghFetch(cfg, `/repos/${cfg.owner}/${cfg.repo}/actions/runs/${runId}/cancel`, {
67
+ method: 'POST',
68
+ });
69
+ }
70
+ function normalize(r) {
71
+ return {
72
+ id: r.id,
73
+ name: r.name,
74
+ workflow: r.path?.replace('.github/workflows/', '') ?? String(r.workflow_id),
75
+ status: normalizeStatus(r.status, r.conclusion),
76
+ branch: r.head_branch,
77
+ commit: r.head_sha?.slice(0, 7),
78
+ url: r.html_url,
79
+ createdAt: r.created_at,
80
+ updatedAt: r.updated_at,
81
+ };
82
+ }
83
+ function normalizeStatus(status, conclusion) {
84
+ if (status === 'completed')
85
+ return conclusion ?? 'completed';
86
+ // queued → pending, in_progress → running
87
+ if (status === 'queued' || status === 'waiting')
88
+ return 'pending';
89
+ if (status === 'in_progress')
90
+ return 'running';
91
+ return status;
92
+ }
@@ -0,0 +1,20 @@
1
+ import type { CiConfig, NormalizedBuild } from './config.js';
2
+ export interface GitLabWorkflowInfo {
3
+ schedules: Array<{
4
+ id: number;
5
+ description: string;
6
+ ref: string;
7
+ cron: string;
8
+ active: boolean;
9
+ }>;
10
+ triggers: Array<{
11
+ id: number;
12
+ description: string;
13
+ }>;
14
+ note: string;
15
+ }
16
+ export declare function listWorkflows(cfg: CiConfig): Promise<GitLabWorkflowInfo>;
17
+ export declare function triggerBuild(cfg: CiConfig, ref?: string, variables?: Record<string, string>): Promise<NormalizedBuild>;
18
+ export declare function getBuildStatus(cfg: CiConfig, pipelineId: string | number): Promise<NormalizedBuild>;
19
+ export declare function listRecentBuilds(cfg: CiConfig, ref?: string, limit?: number): Promise<NormalizedBuild[]>;
20
+ export declare function cancelBuild(cfg: CiConfig, pipelineId: string | number): Promise<void>;
@@ -0,0 +1,99 @@
1
+ function base(cfg) {
2
+ return `${cfg.host ?? 'https://gitlab.com'}/api/v4`;
3
+ }
4
+ // GitLab accepts both numeric project ID and "namespace%2Frepo" path
5
+ function projectId(cfg) {
6
+ return encodeURIComponent(`${cfg.owner}/${cfg.repo}`);
7
+ }
8
+ function headers(token) {
9
+ return {
10
+ 'PRIVATE-TOKEN': token,
11
+ 'Content-Type': 'application/json',
12
+ };
13
+ }
14
+ async function glFetch(cfg, endpoint, options) {
15
+ const res = await fetch(`${base(cfg)}${endpoint}`, {
16
+ ...options,
17
+ headers: { ...headers(cfg.token), ...(options?.headers ?? {}) },
18
+ });
19
+ if (res.status === 204)
20
+ return null;
21
+ const body = await res.text();
22
+ if (!res.ok)
23
+ throw new Error(`GitLab API ${res.status}: ${body}`);
24
+ return JSON.parse(body);
25
+ }
26
+ export async function listWorkflows(cfg) {
27
+ const [schedules, triggers] = await Promise.all([
28
+ glFetch(cfg, `/projects/${projectId(cfg)}/pipeline_schedules`),
29
+ glFetch(cfg, `/projects/${projectId(cfg)}/triggers`),
30
+ ]);
31
+ return {
32
+ schedules: schedules.map((s) => ({
33
+ id: s.id,
34
+ description: s.description,
35
+ ref: s.ref,
36
+ cron: s.cron,
37
+ active: s.active,
38
+ })),
39
+ triggers: triggers.map((t) => ({
40
+ id: t.id,
41
+ description: t.description ?? '',
42
+ })),
43
+ note: 'GitLab은 workflow 파일 개념이 없습니다. ci_trigger_build(ref="main")로 해당 브랜치의 .gitlab-ci.yml을 즉시 실행하세요.',
44
+ };
45
+ }
46
+ export async function triggerBuild(cfg, ref = 'main', variables = {}) {
47
+ const vars = Object.entries(variables).map(([key, value]) => ({ key, value }));
48
+ const body = { ref };
49
+ if (vars.length > 0)
50
+ body.variables = vars;
51
+ const data = await glFetch(cfg, `/projects/${projectId(cfg)}/pipeline`, {
52
+ method: 'POST',
53
+ body: JSON.stringify(body),
54
+ });
55
+ return normalize(data);
56
+ }
57
+ export async function getBuildStatus(cfg, pipelineId) {
58
+ const data = await glFetch(cfg, `/projects/${projectId(cfg)}/pipelines/${pipelineId}`);
59
+ return normalize(data);
60
+ }
61
+ export async function listRecentBuilds(cfg, ref, limit = 10) {
62
+ let endpoint = `/projects/${projectId(cfg)}/pipelines?per_page=${limit}&order_by=id&sort=desc`;
63
+ if (ref)
64
+ endpoint += `&ref=${encodeURIComponent(ref)}`;
65
+ const data = await glFetch(cfg, endpoint);
66
+ return data.map(normalize);
67
+ }
68
+ export async function cancelBuild(cfg, pipelineId) {
69
+ await glFetch(cfg, `/projects/${projectId(cfg)}/pipelines/${pipelineId}/cancel`, {
70
+ method: 'POST',
71
+ });
72
+ }
73
+ function normalize(p) {
74
+ return {
75
+ id: p.id,
76
+ status: normalizeStatus(p.status),
77
+ branch: p.ref,
78
+ commit: p.sha?.slice(0, 7),
79
+ url: p.web_url,
80
+ createdAt: p.created_at,
81
+ updatedAt: p.updated_at,
82
+ };
83
+ }
84
+ function normalizeStatus(status) {
85
+ const map = {
86
+ created: 'pending',
87
+ waiting_for_resource: 'pending',
88
+ preparing: 'pending',
89
+ pending: 'pending',
90
+ manual: 'pending',
91
+ scheduled: 'pending',
92
+ running: 'running',
93
+ success: 'success',
94
+ failed: 'failed',
95
+ canceled: 'cancelled',
96
+ skipped: 'cancelled',
97
+ };
98
+ return map[status] ?? status;
99
+ }
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { registerChecksTools } from './registers/checks.js';
10
10
  import { registerAiTools } from './registers/ai.js';
11
11
  import { registerBigqueryTools } from './registers/bigquery.js';
12
12
  import { registerAuthTools } from './registers/auth.js';
13
+ import { registerCiTools } from './registers/ci.js';
13
14
  import { registerPrompts } from './prompts.js';
14
15
  import { registerResources } from './resources.js';
15
16
  const server = new McpServer({
@@ -25,6 +26,7 @@ registerChecksTools(server);
25
26
  registerAiTools(server);
26
27
  registerBigqueryTools(server);
27
28
  registerAuthTools(server);
29
+ registerCiTools(server);
28
30
  registerPrompts(server);
29
31
  registerResources(server);
30
32
  // `npx -y @yoonion/mimi-seed-mcp <subcommand>` 처리.
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerCiTools(server: McpServer): void;
@@ -0,0 +1,171 @@
1
+ import { z } from 'zod';
2
+ import { requireCiConfig, saveCiConfig } from '../ci/config.js';
3
+ import * as github from '../ci/github.js';
4
+ import * as gitlab from '../ci/gitlab.js';
5
+ export function registerCiTools(server) {
6
+ server.tool('ci_save_config', [
7
+ 'GitHub Actions 또는 GitLab CI 연결 설정을 저장합니다.',
8
+ '저장 위치: ~/.mimi-seed/ci.json (mode 0600).',
9
+ 'GitHub: provider="github", token="ghp_..." (repo+workflow 스코프 필요)',
10
+ 'GitHub Enterprise: host="https://github.example.com" 추가',
11
+ 'GitLab.com: provider="gitlab", token="glpat-..."',
12
+ 'Self-hosted GitLab: host="https://gitlab.example.com" 추가',
13
+ ].join(' '), {
14
+ provider: z.enum(['github', 'gitlab']).describe('CI 프로바이더'),
15
+ token: z.string().describe('Personal Access Token'),
16
+ owner: z.string().describe('GitHub org/user 또는 GitLab namespace'),
17
+ repo: z.string().describe('저장소 이름 (경로 없이 repo명만)'),
18
+ host: z.string().optional().describe('GitHub Enterprise 또는 GitLab self-hosted URL'),
19
+ }, async ({ provider, token, owner, repo, host }) => {
20
+ const config = { provider, token, owner, repo, host };
21
+ saveCiConfig(config);
22
+ return {
23
+ content: [{
24
+ type: 'text',
25
+ text: [
26
+ `✅ CI 설정 저장 완료 (${provider})`,
27
+ ` 저장소: ${owner}/${repo}`,
28
+ host ? ` Host: ${host}` : '',
29
+ '',
30
+ 'ci_list_workflows 로 사용 가능한 워크플로를 확인하세요.',
31
+ ].filter(Boolean).join('\n'),
32
+ }],
33
+ };
34
+ });
35
+ server.tool('ci_list_workflows', [
36
+ 'GitHub Actions 워크플로 목록 또는 GitLab 파이프라인 스케줄/트리거 목록을 조회합니다.',
37
+ 'ci_trigger_build의 workflow 파라미터에 파일명(deploy.yml)을 사용하세요.',
38
+ ].join(' '), {}, async () => {
39
+ const cfg = requireCiConfig();
40
+ const result = cfg.provider === 'github'
41
+ ? await github.listWorkflows(cfg)
42
+ : await gitlab.listWorkflows(cfg);
43
+ return {
44
+ content: [{
45
+ type: 'text',
46
+ text: JSON.stringify(result, null, 2),
47
+ }],
48
+ };
49
+ });
50
+ server.tool('ci_trigger_build', [
51
+ '빌드를 트리거합니다.',
52
+ 'GitHub: workflow 필수 (파일명 "deploy.yml" 또는 숫자 ID). workflow_dispatch 트리거가 설정된 워크플로만 실행 가능.',
53
+ 'GitLab: workflow 불필요 — ref(브랜치)만 지정하면 .gitlab-ci.yml 즉시 실행.',
54
+ '완료 직후 최신 빌드 정보를 반환합니다. run_id를 ci_get_build_status에 사용하세요.',
55
+ ].join(' '), {
56
+ ref: z.string().default('main').describe('브랜치 또는 태그 (기본: main)'),
57
+ workflow: z.string().optional().describe('GitHub 전용: 워크플로 파일명 또는 ID (예: deploy.yml)'),
58
+ inputs: z.record(z.string()).optional().describe('워크플로 입력값 (GitHub inputs / GitLab variables)'),
59
+ }, async ({ ref, workflow, inputs }) => {
60
+ const cfg = requireCiConfig();
61
+ let result;
62
+ if (cfg.provider === 'github') {
63
+ if (!workflow)
64
+ throw new Error('GitHub 빌드 트리거에는 workflow 파라미터가 필요합니다. (예: "deploy.yml")');
65
+ result = await github.triggerBuild(cfg, workflow, ref, inputs ?? {});
66
+ }
67
+ else {
68
+ result = await gitlab.triggerBuild(cfg, ref, inputs ?? {});
69
+ }
70
+ if (!result) {
71
+ return {
72
+ content: [{
73
+ type: 'text',
74
+ text: '✅ 빌드 트리거 완료. run_id 조회 불가 — 잠시 후 ci_list_recent_builds로 확인하세요.',
75
+ }],
76
+ };
77
+ }
78
+ return {
79
+ content: [{
80
+ type: 'text',
81
+ text: [
82
+ `✅ 빌드 트리거 완료`,
83
+ ` run_id: ${result.id}`,
84
+ ` 상태: ${result.status}`,
85
+ ` 브랜치: ${result.branch}`,
86
+ result.commit ? ` 커밋: ${result.commit}` : '',
87
+ ` URL: ${result.url}`,
88
+ '',
89
+ `ci_get_build_status(run_id="${result.id}") 로 진행 상황을 확인하세요.`,
90
+ ].filter(Boolean).join('\n'),
91
+ }],
92
+ };
93
+ });
94
+ server.tool('ci_get_build_status', '특정 빌드(GitHub Actions run / GitLab pipeline)의 현재 상태를 조회합니다.', {
95
+ run_id: z.string().describe('빌드 ID (ci_trigger_build 또는 ci_list_recent_builds 반환값)'),
96
+ }, async ({ run_id }) => {
97
+ const cfg = requireCiConfig();
98
+ const build = cfg.provider === 'github'
99
+ ? await github.getBuildStatus(cfg, run_id)
100
+ : await gitlab.getBuildStatus(cfg, run_id);
101
+ const statusEmoji = {
102
+ pending: '⏳', running: '🔄', success: '✅', failed: '❌', cancelled: '⛔',
103
+ };
104
+ const emoji = statusEmoji[build.status] ?? '❓';
105
+ return {
106
+ content: [{
107
+ type: 'text',
108
+ text: [
109
+ `${emoji} 빌드 #${build.id} — ${build.status.toUpperCase()}`,
110
+ build.workflow ? ` 워크플로: ${build.workflow}` : '',
111
+ ` 브랜치: ${build.branch}`,
112
+ build.commit ? ` 커밋: ${build.commit}` : '',
113
+ ` 생성: ${build.createdAt}`,
114
+ ` 갱신: ${build.updatedAt}`,
115
+ ` URL: ${build.url}`,
116
+ ].filter(Boolean).join('\n'),
117
+ }],
118
+ };
119
+ });
120
+ server.tool('ci_list_recent_builds', '최근 빌드 목록을 조회합니다. 특정 브랜치나 워크플로로 필터링 가능.', {
121
+ workflow: z.string().optional().describe('GitHub 전용: 워크플로 파일명 또는 ID'),
122
+ ref: z.string().optional().describe('GitLab 전용: 브랜치 필터'),
123
+ limit: z.number().int().min(1).max(50).default(10).describe('최대 개수 (기본: 10)'),
124
+ }, async ({ workflow, ref, limit }) => {
125
+ const cfg = requireCiConfig();
126
+ const builds = cfg.provider === 'github'
127
+ ? await github.listRecentBuilds(cfg, workflow, limit)
128
+ : await gitlab.listRecentBuilds(cfg, ref, limit);
129
+ if (builds.length === 0) {
130
+ return { content: [{ type: 'text', text: '최근 빌드 없음.' }] };
131
+ }
132
+ const statusEmoji = {
133
+ pending: '⏳', running: '🔄', success: '✅', failed: '❌', cancelled: '⛔',
134
+ };
135
+ const lines = builds.map((b) => {
136
+ const emoji = statusEmoji[b.status] ?? '❓';
137
+ const parts = [
138
+ `${emoji} #${b.id}`,
139
+ b.workflow ? `[${b.workflow}]` : '',
140
+ b.branch,
141
+ b.commit ? `@${b.commit}` : '',
142
+ `→ ${b.status}`,
143
+ `(${new Date(b.createdAt).toLocaleString('ko-KR')})`,
144
+ ].filter(Boolean);
145
+ return parts.join(' ');
146
+ });
147
+ return {
148
+ content: [{
149
+ type: 'text',
150
+ text: `최근 빌드 ${builds.length}개:\n\n${lines.join('\n')}`,
151
+ }],
152
+ };
153
+ });
154
+ server.tool('ci_cancel_build', '진행 중인 빌드를 취소합니다. (GitHub Actions run / GitLab pipeline)', {
155
+ run_id: z.string().describe('빌드 ID (ci_trigger_build 또는 ci_list_recent_builds 반환값)'),
156
+ }, async ({ run_id }) => {
157
+ const cfg = requireCiConfig();
158
+ if (cfg.provider === 'github') {
159
+ await github.cancelBuild(cfg, run_id);
160
+ }
161
+ else {
162
+ await gitlab.cancelBuild(cfg, run_id);
163
+ }
164
+ return {
165
+ content: [{
166
+ type: 'text',
167
+ text: `⛔ 빌드 #${run_id} 취소 요청 완료.\nci_get_build_status(run_id="${run_id}") 로 상태를 확인하세요.`,
168
+ }],
169
+ };
170
+ });
171
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
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": {