Skip to content

Commit 632366f

Browse files
authored
Merge pull request #932 from Kyoungwoong/feature/link-newviz
feat: (vibe) React Component와 MCP 서버 연동.
2 parents 8e17c0e + 092adf5 commit 632366f

6 files changed

Lines changed: 959 additions & 1 deletion

File tree

packages/mcp/src/common/types.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,74 @@ export interface FileChangeInfo {
5555
deletions?: number;
5656
changes?: number;
5757
}
58+
59+
export interface CSMDictGeneratorInputs {
60+
repo: string;
61+
githubToken: string;
62+
baseBranchName?: string;
63+
locale?: string;
64+
debug?: boolean;
65+
}
66+
67+
export interface AnalysisResult {
68+
isPRSuccess: boolean;
69+
csmDict: Record<string, unknown[]>;
70+
}
71+
72+
export interface CSMDictResult {
73+
success: boolean;
74+
data: {
75+
repository: GitHubRepoInfo & { url: string };
76+
analysis: {
77+
baseBranch: string;
78+
isPRDataAvailable: boolean;
79+
branches: string[];
80+
totalClusters: number;
81+
csmDict: Record<string, unknown[]>;
82+
};
83+
};
84+
metadata: {
85+
analyzedAt: string;
86+
commitsProcessed: number;
87+
debugMode: boolean;
88+
version?: string;
89+
};
90+
}
91+
92+
// React Component Test Types
93+
export interface ReactComponentTestInputs {
94+
complexity?: "simple" | "medium" | "complex" | "all";
95+
componentType?: "basic" | "chart" | "form" | "data-display" | "interactive";
96+
}
97+
98+
export interface ReactComponentDefinition {
99+
title: string;
100+
description: string;
101+
component: string;
102+
usage: string;
103+
}
104+
105+
export interface ReactComponentTestResult {
106+
components: ReactComponentDefinition[];
107+
testQuestions: string[];
108+
}
109+
110+
// Data-driven React Component Types
111+
export interface DataDrivenComponentInputs {
112+
dataType?: "chart" | "table" | "list" | "card" | "all";
113+
sampleData?: boolean;
114+
}
115+
116+
export interface DataDrivenComponentDefinition {
117+
title: string;
118+
description: string;
119+
component: string;
120+
usage: string;
121+
sampleData: any;
122+
dataStructure: string;
123+
}
124+
125+
export interface DataDrivenComponentResult {
126+
components: DataDrivenComponentDefinition[];
127+
testQuestions: string[];
128+
}

packages/mcp/src/common/utils.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,142 @@ export const GitHubUtils = {
100100
return null;
101101
},
102102

103+
async fetchGitLogFromGitHub(githubToken: string, owner: string, repo: string): Promise<string> {
104+
const octokit = this.createGitHubAPIClient(githubToken);
105+
106+
try {
107+
const repoInfo = await octokit.repos.get({ owner, repo });
108+
const defaultBranch = repoInfo.data.default_branch;
109+
const branches = await octokit.repos.listBranches({ owner, repo });
110+
111+
let allCommits: any[] = [];
112+
const branchCommitMap = new Map<string, string[]>();
113+
114+
for (const branch of branches.data) {
115+
try {
116+
let page = 1;
117+
let hasMoreCommits = true;
118+
const branchCommits: string[] = [];
119+
120+
while (hasMoreCommits && page <= 5) {
121+
const commits = await octokit.repos.listCommits({
122+
owner,
123+
repo,
124+
sha: branch.name,
125+
per_page: 100,
126+
page
127+
});
128+
129+
allCommits.push(...commits.data.map(commit => ({ ...commit, branchName: branch.name })));
130+
branchCommits.push(...commits.data.map(commit => commit.sha));
131+
hasMoreCommits = commits.data.length === 100;
132+
page++;
133+
}
134+
135+
branchCommitMap.set(branch.name, branchCommits);
136+
} catch (error) {
137+
console.debug(`Failed to fetch commits for branch ${branch.name}:`, error);
138+
}
139+
}
140+
141+
const uniqueCommits = Array.from(
142+
new Map(allCommits.map(commit => [commit.sha, commit])).values()
143+
);
144+
145+
uniqueCommits.sort((a, b) =>
146+
new Date(a.commit.committer.date).getTime() - new Date(b.commit.committer.date).getTime()
147+
);
148+
149+
const gitLogEntries = await Promise.all(
150+
uniqueCommits.slice(0, 1000).map(async (commit) => {
151+
const hash = commit.sha;
152+
const parents = commit.parents.map((p: any) => p.sha).join(' ');
153+
154+
const refs = [];
155+
for (const [branchName, commits] of branchCommitMap.entries()) {
156+
if (commits.includes(hash)) {
157+
if (branchName === defaultBranch) {
158+
refs.push(`origin/${branchName}`, branchName);
159+
} else {
160+
refs.push(`origin/${branchName}`);
161+
}
162+
}
163+
}
164+
165+
if (refs.length === 0 && commit.branchName) {
166+
refs.push(`origin/${commit.branchName}`);
167+
}
168+
169+
const refString = refs.join(', ');
170+
const authorName = commit.commit.author?.name || '';
171+
const authorEmail = commit.commit.author?.email || '';
172+
const authorDate = commit.commit.author?.date || '';
173+
const committerName = commit.commit.committer?.name || '';
174+
const committerEmail = commit.commit.committer?.email || '';
175+
const committerDate = commit.commit.committer?.date || '';
176+
const message = commit.commit.message || '';
177+
const messageLines = message.split('\n');
178+
const subject = messageLines[0] || '';
179+
const body = messageLines.slice(1).join('\n').trim();
180+
181+
let fileStats = '';
182+
try {
183+
const commitDetail = await octokit.repos.getCommit({
184+
owner,
185+
repo,
186+
ref: hash
187+
});
188+
189+
if (commitDetail.data.files && commitDetail.data.files.length > 0) {
190+
fileStats = '\n' + commitDetail.data.files.map((file: any) => {
191+
const additions = file.additions || 0;
192+
const deletions = file.deletions || 0;
193+
const filename = file.filename || '';
194+
return `${additions}\t${deletions}\t${filename}`;
195+
}).join('\n');
196+
}
197+
} catch (error) {
198+
console.debug(`Failed to fetch file stats for commit ${hash}`);
199+
}
200+
201+
const commitParts = [
202+
hash,
203+
parents,
204+
refString,
205+
authorName,
206+
authorEmail,
207+
authorDate,
208+
committerName,
209+
committerEmail,
210+
committerDate,
211+
` ${subject}`,
212+
];
213+
214+
if (body) {
215+
commitParts.push(body);
216+
}
217+
218+
return commitParts.join('\n') + fileStats;
219+
})
220+
);
221+
222+
return '\n\n' + gitLogEntries.join('\n\n\n\n');
223+
} catch (error: any) {
224+
throw new Error(`Failed to fetch commits from GitHub: ${error.message}`);
225+
}
226+
},
227+
228+
async getDefaultBranch(githubToken: string, owner: string, repo: string): Promise<string> {
229+
const octokit = this.createGitHubAPIClient(githubToken);
230+
231+
try {
232+
const repoInfo = await octokit.repos.get({ owner, repo });
233+
return repoInfo.data.default_branch;
234+
} catch (error: any) {
235+
throw new Error(`Failed to get default branch: ${error.message}`);
236+
}
237+
},
238+
103239
async safeApiCall<T>(
104240
apiCall: () => Promise<T>,
105241
errorMessage: string
@@ -143,3 +279,4 @@ export const CommonUtils = {
143279
return isNaN(parsed) ? 0 : parsed;
144280
}
145281
};
282+

packages/mcp/src/server.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import fs from "node:fs/promises";
55
import path from "node:path";
66
import { analyzeFeatureImpact } from "./tool/featureImpactAnalyzer.js";
77
import { recommendContributors } from "./tool/contributorRecommender.js";
8-
import type { FeatureImpactAnalyzerInputs, ContributorRecommenderInputs } from "./common/types.js";
8+
import { testReactComponents } from "./tool/reactComponentTester.js";
9+
import { testDataDrivenComponents } from "./tool/dataDrivenComponentTester.js";
10+
import type { FeatureImpactAnalyzerInputs, ContributorRecommenderInputs, ReactComponentTestInputs, DataDrivenComponentInputs } from "./common/types.js";
911
import { I18n } from "./common/i18n.js";
12+
import { generateNewViz } from "./tool/generateNewViz.js";
1013

1114
const server = new McpServer({
1215
name: "githru-mcp",
@@ -384,6 +387,120 @@ server.registerTool(
384387
}
385388
);
386389

390+
server.registerTool(
391+
"generate_csm_dict",
392+
{
393+
title: "Generate CSM Dictionary",
394+
description: "Generates CSM (Commit Sequence Map) Dictionary from GitHub repository using AnalysisEngine",
395+
inputSchema: {
396+
repo: z.string().describe("GitHub repository (format: 'owner/repo' or 'https://github.com/owner/repo')"),
397+
githubToken: z.string().describe("GitHub authentication token"),
398+
baseBranchName: z.string().optional().describe("Base branch name to analyze (default: repository's default branch)"),
399+
locale: z.enum(["en", "ko"]).default("en").describe("Response language (en: English, ko: Korean)"),
400+
debug: z.boolean().default(false).describe("Enable debug mode for detailed logging"),
401+
},
402+
},
403+
404+
async ({ repo, githubToken, baseBranchName, locale, debug }: {
405+
repo: string;
406+
githubToken: string;
407+
baseBranchName?: string;
408+
locale?: string;
409+
debug?: boolean;
410+
}) => {
411+
try {
412+
const result = await generateNewViz({
413+
repo,
414+
githubToken,
415+
baseBranchName,
416+
locale,
417+
debug
418+
});
419+
420+
return {
421+
content: [
422+
{
423+
type: "text",
424+
text: JSON.stringify(result, null, 2),
425+
},
426+
],
427+
};
428+
} catch (err: any) {
429+
return {
430+
content: [
431+
{ type: "text", text: `CSM Dictionary generation error: ${err?.message ?? String(err)}` },
432+
],
433+
};
434+
}
435+
}
436+
);
437+
438+
server.registerTool(
439+
"react_component_test",
440+
{
441+
title: "React Component Test",
442+
description: "Provides React components of varying complexity to test Claude's understanding capabilities",
443+
inputSchema: {
444+
complexity: z.enum(["simple", "medium", "complex", "all"]).default("simple").describe("Complexity level of React components to test"),
445+
componentType: z.enum(["basic", "chart", "form", "data-display", "interactive"]).optional().describe("Type of component to generate"),
446+
},
447+
},
448+
449+
async ({ complexity, componentType }: ReactComponentTestInputs) => {
450+
try {
451+
const result = await testReactComponents({ complexity, componentType });
452+
453+
return {
454+
content: [
455+
{
456+
type: "text",
457+
text: result,
458+
},
459+
],
460+
};
461+
} catch (err: any) {
462+
return {
463+
content: [
464+
{ type: "text", text: `React component test error: ${err?.message ?? String(err)}` },
465+
],
466+
};
467+
}
468+
}
469+
);
470+
471+
server.registerTool(
472+
"data_driven_component_test",
473+
{
474+
title: "Data-Driven React Component Test",
475+
description: "Provides React components that work with data props to test Claude's understanding of data-driven patterns",
476+
inputSchema: {
477+
dataType: z.enum(["chart", "table", "list", "card", "all"]).default("all").describe("Type of data-driven component to test"),
478+
sampleData: z.boolean().default(true).describe("Include sample data with components"),
479+
},
480+
},
481+
482+
async ({ dataType, sampleData }: DataDrivenComponentInputs) => {
483+
try {
484+
const result = await testDataDrivenComponents({ dataType, sampleData });
485+
486+
return {
487+
content: [
488+
{
489+
type: "text",
490+
text: result,
491+
},
492+
],
493+
};
494+
} catch (err: any) {
495+
return {
496+
content: [
497+
{ type: "text", text: `Data-driven component test error: ${err?.message ?? String(err)}` },
498+
],
499+
};
500+
}
501+
}
502+
);
503+
387504
async function main() {
388505
const transport = new StdioServerTransport();
389506
await server.connect(transport);

0 commit comments

Comments
 (0)