mcp-integration-server-solin
Version:
MCP Server for Cody, YouTrack, and Forgejo integration
205 lines • 7.98 kB
JavaScript
import axios from 'axios';
export class YouTrackService {
client;
baseUrl;
token;
constructor() {
this.baseUrl = process.env.YOUTRACK_BASE_URL || '';
this.token = process.env.YOUTRACK_TOKEN || '';
if (!this.baseUrl || !this.token) {
throw new Error('YouTrack 설정이 필요합니다: YOUTRACK_BASE_URL, YOUTRACK_TOKEN');
}
this.client = axios.create({
baseURL: this.baseUrl,
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
}
async getIssues(args) {
try {
const { project, query = '', limit = 10 } = args;
const searchQuery = query ? `project: ${project} ${query}` : `project: ${project}`;
const response = await this.client.get('/api/issues', {
params: {
query: searchQuery,
$top: limit,
fields: 'id,idReadable,summary,description,created,updated,resolved,state(name),priority(name),type(name)',
},
});
// YouTrack API 응답 구조 처리
let issuesData = response.data;
// 응답이 객체이고 issues 프로퍼티가 있는 경우
if (issuesData && typeof issuesData === 'object' && !Array.isArray(issuesData)) {
if (issuesData.issues) {
issuesData = issuesData.issues;
}
else if (issuesData.value) {
issuesData = issuesData.value;
}
else {
// 단일 객체인 경우 배열로 변환
issuesData = [issuesData];
}
}
// 배열이 아닌 경우 빈 배열로 처리
if (!Array.isArray(issuesData)) {
issuesData = [];
}
const issues = issuesData.map((issue) => ({
id: issue.idReadable,
summary: issue.summary,
description: issue.description,
state: issue.state?.name,
priority: issue.priority?.name,
type: issue.type?.name,
created: issue.created,
updated: issue.updated,
resolved: issue.resolved,
}));
return {
content: [
{
type: 'text',
text: `YouTrack 이슈 목록 (${issues.length}개):\n\n${issues
.map((issue) => `• ${issue.id}: ${issue.summary} [${issue.state}]`)
.join('\n')}`,
},
],
};
}
catch (error) {
throw new Error(`YouTrack 이슈 조회 실패: ${error instanceof Error ? error.message : String(error)}`);
}
}
async getIssue(args) {
try {
const { issueId } = args;
// 디버깅: 요청 정보 출력
console.log('YouTrack 요청 URL:', `${this.baseUrl}/api/issues/${issueId}`);
console.log('YouTrack 인증 헤더:', this.client.defaults.headers['Authorization']);
const response = await this.client.get(`/api/issues/${issueId}`, {
params: {
fields: 'id,idReadable,summary,description,created,updated,resolved,state(name),priority(name),type(name),assignee(name),reporter(name)',
},
});
// 디버깅: 실제 응답 구조 확인
console.log('YouTrack API 응답:', JSON.stringify(response.data, null, 2));
const issue = response.data;
return {
content: [
{
type: 'text',
text: `YouTrack 이슈 상세정보:
ID: ${issue.idReadable}
제목: ${issue.summary}
설명: ${issue.description || '설명 없음'}
상태: ${issue.state?.name || '알 수 없음'}
우선순위: ${issue.priority?.name || '알 수 없음'}
타입: ${issue.type?.name || '알 수 없음'}
담당자: ${issue.assignee?.name || '미할당'}
보고자: ${issue.reporter?.name || '알 수 없음'}
생성일: ${new Date(issue.created).toLocaleString()}
수정일: ${new Date(issue.updated).toLocaleString()}`,
},
],
};
}
catch (error) {
throw new Error(`YouTrack 이슈 조회 실패: ${error instanceof Error ? error.message : String(error)}`);
}
}
async createIssue(args) {
try {
const { project, summary, description, type = 'Task', priority = 'Normal' } = args;
const issueData = {
project: { id: project },
summary,
description,
};
const response = await this.client.post('/api/issues', issueData, {
params: {
fields: 'id,idReadable,summary',
},
});
const issue = response.data;
// 타입과 우선순위 설정 (별도 요청)
if (type !== 'Task') {
await this.updateIssueField(issue.idReadable, 'Type', type);
}
if (priority !== 'Normal') {
await this.updateIssueField(issue.idReadable, 'Priority', priority);
}
return {
content: [
{
type: 'text',
text: `YouTrack 이슈가 생성되었습니다: ${issue.idReadable} - ${issue.summary}`,
},
],
};
}
catch (error) {
throw new Error(`YouTrack 이슈 생성 실패: ${error instanceof Error ? error.message : String(error)}`);
}
}
async updateIssue(args) {
try {
const { issueId, summary, description, state } = args;
const updates = {};
if (summary)
updates.summary = summary;
if (description)
updates.description = description;
if (Object.keys(updates).length > 0) {
await this.client.post(`/api/issues/${issueId}`, updates);
}
if (state) {
await this.updateIssueField(issueId, 'State', state);
}
return {
content: [
{
type: 'text',
text: `YouTrack 이슈 ${issueId}가 업데이트되었습니다.`,
},
],
};
}
catch (error) {
throw new Error(`YouTrack 이슈 업데이트 실패: ${error instanceof Error ? error.message : String(error)}`);
}
}
async addComment(args) {
try {
const { issueId, comment } = args;
await this.client.post(`/api/issues/${issueId}/comments`, {
text: comment,
});
return {
content: [
{
type: 'text',
text: `YouTrack 이슈 ${issueId}에 댓글이 추가되었습니다.`,
},
],
};
}
catch (error) {
throw new Error(`YouTrack 댓글 추가 실패: ${error instanceof Error ? error.message : String(error)}`);
}
}
async updateIssueField(issueId, fieldName, value) {
try {
await this.client.post(`/api/issues/${issueId}/fields/${fieldName}`, {
value: { name: value },
});
}
catch (error) {
console.error(`필드 ${fieldName} 업데이트 실패:`, error);
}
}
}
//# sourceMappingURL=youtrack.js.map