@nseed/capman-feed-mcp 1.0.0

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.
Files changed (4) hide show
  1. package/README.md +135 -0
  2. package/api.js +89 -0
  3. package/index.js +378 -0
  4. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # Capman Feed MCP Server
2
+
3
+ Claude Code에서 Capman Feed API를 통해 피드백을 조회/관리할 수 있게 해주는 MCP 서버입니다.
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ cd capman-feed-mcp
9
+ npm install
10
+ ```
11
+
12
+ ## 설정
13
+
14
+ ### Claude Code 설정
15
+
16
+ `~/.claude/claude_desktop_config.json` 또는 프로젝트의 `.claude/settings.json`에 추가:
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "capman-feed": {
22
+ "command": "node",
23
+ "args": ["/path/to/capman-feed-mcp/index.js"],
24
+ "env": {
25
+ "CAPMAN_API_KEY": "cf_your_api_key_here"
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### 환경 변수
33
+
34
+ | 변수 | 필수 | 설명 |
35
+ |------|------|------|
36
+ | `CAPMAN_API_KEY` | ✅ | Capman Feed API 키 |
37
+ | `CAPMAN_BASE_URL` | ❌ | API 기본 URL (기본: https://capman.kr/api/v1/feedback) |
38
+
39
+ ## 사용 가능한 도구
40
+
41
+ ### get_feedbacks
42
+
43
+ 피드백 목록을 조회합니다.
44
+
45
+ ```
46
+ 파라미터:
47
+ - status: open, in_progress, resolved, closed
48
+ - type: bug, feature, improvement, question
49
+ - priority: low, medium, high, critical
50
+ - search: 검색어
51
+ - limit: 조회 개수 (기본 10)
52
+ ```
53
+
54
+ ### get_feedback_detail
55
+
56
+ 피드백 상세 정보를 조회합니다. 콘솔 로그, 네트워크 에러, 환경 정보 포함.
57
+
58
+ ```
59
+ 파라미터:
60
+ - id: 피드백 ID (필수)
61
+ ```
62
+
63
+ ### update_feedback_status
64
+
65
+ 피드백 상태를 변경합니다.
66
+
67
+ ```
68
+ 파라미터:
69
+ - id: 피드백 ID (필수)
70
+ - status: 변경할 상태 (필수)
71
+ ```
72
+
73
+ ### add_comment
74
+
75
+ 피드백에 댓글을 추가합니다.
76
+
77
+ ```
78
+ 파라미터:
79
+ - id: 피드백 ID (필수)
80
+ - content: 댓글 내용 (필수)
81
+ ```
82
+
83
+ ### get_stats
84
+
85
+ 피드백 통계를 조회합니다.
86
+
87
+ ### analyze_feedback
88
+
89
+ 피드백의 콘솔 로그와 네트워크 에러를 분석하여 문제 원인을 파악합니다.
90
+
91
+ ```
92
+ 파라미터:
93
+ - id: 분석할 피드백 ID (필수)
94
+ ```
95
+
96
+ ## 사용 예시
97
+
98
+ Claude Code에서:
99
+
100
+ ```
101
+ 사용자: 새로운 버그 피드백 확인해줘
102
+
103
+ Claude: [get_feedbacks 호출 - status: open, type: bug]
104
+ 3개의 미해결 버그가 있습니다:
105
+ 1. [bug] 결제 버튼 클릭 안됨 (high)
106
+ 2. [bug] 로그인 후 리다이렉트 오류 (medium)
107
+ ...
108
+
109
+ 사용자: 첫 번째 버그 분석해줘
110
+
111
+ Claude: [analyze_feedback 호출]
112
+ ## 에러 분석
113
+ TypeError: Cannot read property 'type' of undefined
114
+ → 파일: src/pages/checkout.js:142
115
+
116
+ ## 권장 조치
117
+ paymentMethod가 undefined인 경우 처리 필요
118
+
119
+ 사용자: 코드 수정하고 resolved로 변경해줘
120
+
121
+ Claude: [코드 수정 후]
122
+ [update_feedback_status 호출 - status: resolved]
123
+ [add_comment 호출 - "paymentMethod null 체크 추가 (checkout.js:142)"]
124
+ ```
125
+
126
+ ## 개발
127
+
128
+ ```bash
129
+ # 로컬 테스트
130
+ CAPMAN_API_KEY=your_key node index.js
131
+ ```
132
+
133
+ ## 라이센스
134
+
135
+ MIT
package/api.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Capman Feed API Client
3
+ */
4
+
5
+ const DEFAULT_BASE_URL = 'https://capman.kr/api/v1/feedback';
6
+
7
+ export class CapmanFeedAPI {
8
+ constructor(apiKey, baseUrl = DEFAULT_BASE_URL) {
9
+ this.apiKey = apiKey;
10
+ this.baseUrl = baseUrl;
11
+ }
12
+
13
+ async request(endpoint, options = {}) {
14
+ const url = `${this.baseUrl}${endpoint}`;
15
+ const response = await fetch(url, {
16
+ ...options,
17
+ headers: {
18
+ 'X-API-Key': this.apiKey,
19
+ 'Content-Type': 'application/json',
20
+ ...options.headers,
21
+ },
22
+ });
23
+
24
+ if (!response.ok) {
25
+ const error = await response.text();
26
+ throw new Error(`API Error ${response.status}: ${error}`);
27
+ }
28
+
29
+ return response.json();
30
+ }
31
+
32
+ /**
33
+ * 피드백 목록 조회
34
+ */
35
+ async getFeedbacks(params = {}) {
36
+ const query = new URLSearchParams();
37
+
38
+ if (params.status) query.set('status', params.status);
39
+ if (params.type) query.set('type', params.type);
40
+ if (params.priority) query.set('priority', params.priority);
41
+ if (params.search) query.set('search', params.search);
42
+ if (params.page) query.set('page', params.page);
43
+ if (params.limit) query.set('limit', params.limit || 10);
44
+
45
+ const queryString = query.toString();
46
+ return this.request(`/feedbacks${queryString ? `?${queryString}` : ''}`);
47
+ }
48
+
49
+ /**
50
+ * 피드백 상세 조회
51
+ */
52
+ async getFeedback(id) {
53
+ return this.request(`/feedbacks/${id}`);
54
+ }
55
+
56
+ /**
57
+ * 피드백 수정 (상태, 우선순위 등)
58
+ */
59
+ async updateFeedback(id, data) {
60
+ return this.request(`/feedbacks/${id}`, {
61
+ method: 'PUT',
62
+ body: JSON.stringify(data),
63
+ });
64
+ }
65
+
66
+ /**
67
+ * 댓글 목록 조회
68
+ */
69
+ async getComments(feedbackId) {
70
+ return this.request(`/feedbacks/${feedbackId}/comments`);
71
+ }
72
+
73
+ /**
74
+ * 댓글 추가
75
+ */
76
+ async addComment(feedbackId, content) {
77
+ return this.request(`/feedbacks/${feedbackId}/comments`, {
78
+ method: 'POST',
79
+ body: JSON.stringify({ content }),
80
+ });
81
+ }
82
+
83
+ /**
84
+ * 통계 조회
85
+ */
86
+ async getStats() {
87
+ return this.request('/stats');
88
+ }
89
+ }
package/index.js ADDED
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Capman Feed MCP Server
5
+ *
6
+ * Claude Code에서 Capman Feed API를 통해 피드백을 조회/관리할 수 있게 해주는 MCP 서버
7
+ */
8
+
9
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import {
12
+ CallToolRequestSchema,
13
+ ListToolsRequestSchema,
14
+ } from '@modelcontextprotocol/sdk/types.js';
15
+ import { CapmanFeedAPI } from './api.js';
16
+
17
+ // 환경 변수에서 API 키 읽기
18
+ const API_KEY = process.env.CAPMAN_API_KEY;
19
+ const BASE_URL = process.env.CAPMAN_BASE_URL || 'https://capman.kr/api/v1/feedback';
20
+
21
+ if (!API_KEY) {
22
+ console.error('Error: CAPMAN_API_KEY environment variable is required');
23
+ process.exit(1);
24
+ }
25
+
26
+ const api = new CapmanFeedAPI(API_KEY, BASE_URL);
27
+
28
+ // MCP 서버 생성
29
+ const server = new Server(
30
+ {
31
+ name: 'capman-feed',
32
+ version: '1.0.0',
33
+ },
34
+ {
35
+ capabilities: {
36
+ tools: {},
37
+ },
38
+ }
39
+ );
40
+
41
+ // 도구 목록 정의
42
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
43
+ return {
44
+ tools: [
45
+ {
46
+ name: 'get_feedbacks',
47
+ description: '피드백 목록을 조회합니다. 상태, 유형, 우선순위로 필터링 가능합니다.',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ status: {
52
+ type: 'string',
53
+ description: '상태 필터 (open, in_progress, resolved, closed)',
54
+ enum: ['open', 'in_progress', 'resolved', 'closed'],
55
+ },
56
+ type: {
57
+ type: 'string',
58
+ description: '유형 필터 (bug, feature, improvement, question)',
59
+ enum: ['bug', 'feature', 'improvement', 'question'],
60
+ },
61
+ priority: {
62
+ type: 'string',
63
+ description: '우선순위 필터 (low, medium, high, critical)',
64
+ enum: ['low', 'medium', 'high', 'critical'],
65
+ },
66
+ search: {
67
+ type: 'string',
68
+ description: '제목/설명 검색어',
69
+ },
70
+ limit: {
71
+ type: 'number',
72
+ description: '조회 개수 (기본: 10, 최대: 50)',
73
+ default: 10,
74
+ },
75
+ },
76
+ },
77
+ },
78
+ {
79
+ name: 'get_feedback_detail',
80
+ description: '피드백 상세 정보를 조회합니다. 콘솔 로그, 네트워크 에러, 환경 정보 등 포함.',
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ id: {
85
+ type: 'string',
86
+ description: '피드백 ID',
87
+ },
88
+ },
89
+ required: ['id'],
90
+ },
91
+ },
92
+ {
93
+ name: 'update_feedback_status',
94
+ description: '피드백 상태를 변경합니다.',
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ id: {
99
+ type: 'string',
100
+ description: '피드백 ID',
101
+ },
102
+ status: {
103
+ type: 'string',
104
+ description: '변경할 상태',
105
+ enum: ['open', 'in_progress', 'resolved', 'closed'],
106
+ },
107
+ },
108
+ required: ['id', 'status'],
109
+ },
110
+ },
111
+ {
112
+ name: 'add_comment',
113
+ description: '피드백에 댓글을 추가합니다. 분석 결과나 수정 내용을 기록할 때 사용.',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ id: {
118
+ type: 'string',
119
+ description: '피드백 ID',
120
+ },
121
+ content: {
122
+ type: 'string',
123
+ description: '댓글 내용',
124
+ },
125
+ },
126
+ required: ['id', 'content'],
127
+ },
128
+ },
129
+ {
130
+ name: 'get_stats',
131
+ description: '피드백 통계를 조회합니다. 상태별, 유형별 개수 등.',
132
+ inputSchema: {
133
+ type: 'object',
134
+ properties: {},
135
+ },
136
+ },
137
+ {
138
+ name: 'analyze_feedback',
139
+ description: '피드백의 콘솔 로그와 네트워크 에러를 분석하여 문제 원인을 파악합니다.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ id: {
144
+ type: 'string',
145
+ description: '분석할 피드백 ID',
146
+ },
147
+ },
148
+ required: ['id'],
149
+ },
150
+ },
151
+ ],
152
+ };
153
+ });
154
+
155
+ // 도구 실행 핸들러
156
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
157
+ const { name, arguments: args } = request.params;
158
+
159
+ try {
160
+ switch (name) {
161
+ case 'get_feedbacks': {
162
+ const result = await api.getFeedbacks({
163
+ status: args?.status,
164
+ type: args?.type,
165
+ priority: args?.priority,
166
+ search: args?.search,
167
+ limit: args?.limit || 10,
168
+ });
169
+
170
+ // 요약 정보 추가
171
+ const feedbacks = result.data || [];
172
+ const summary = feedbacks.map((f, i) =>
173
+ `${i + 1}. [${f.type}] ${f.title} (${f.status}, ${f.priority})`
174
+ ).join('\n');
175
+
176
+ return {
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: `총 ${result.meta?.total || feedbacks.length}개 중 ${feedbacks.length}개 조회\n\n${summary}\n\n상세 정보가 필요하면 get_feedback_detail을 사용하세요.`,
181
+ },
182
+ {
183
+ type: 'text',
184
+ text: JSON.stringify(result, null, 2),
185
+ },
186
+ ],
187
+ };
188
+ }
189
+
190
+ case 'get_feedback_detail': {
191
+ const result = await api.getFeedback(args.id);
192
+ const feedback = result.data;
193
+
194
+ // 핵심 정보 요약
195
+ let summary = `## ${feedback.title}\n\n`;
196
+ summary += `- **유형**: ${feedback.type}\n`;
197
+ summary += `- **상태**: ${feedback.status}\n`;
198
+ summary += `- **우선순위**: ${feedback.priority}\n`;
199
+ summary += `- **URL**: ${feedback.url}\n`;
200
+ summary += `- **보고자**: ${feedback.reporterName || '익명'}\n\n`;
201
+
202
+ if (feedback.description) {
203
+ summary += `### 설명\n${feedback.description}\n\n`;
204
+ }
205
+
206
+ // 콘솔 로그 (에러/경고만)
207
+ const errors = (feedback.consoleLogs || []).filter(
208
+ (log) => log.level === 'error' || log.level === 'warn'
209
+ );
210
+ if (errors.length > 0) {
211
+ summary += `### 콘솔 에러/경고 (${errors.length}개)\n`;
212
+ errors.slice(0, 10).forEach((log) => {
213
+ summary += `- [${log.level}] ${log.message}\n`;
214
+ });
215
+ summary += '\n';
216
+ }
217
+
218
+ // 네트워크 에러
219
+ if (feedback.networkErrors?.length > 0) {
220
+ summary += `### 네트워크 에러 (${feedback.networkErrors.length}개)\n`;
221
+ feedback.networkErrors.slice(0, 5).forEach((err) => {
222
+ summary += `- ${err.method} ${err.url} → ${err.status}\n`;
223
+ });
224
+ summary += '\n';
225
+ }
226
+
227
+ // 환경 정보
228
+ if (feedback.metadata) {
229
+ summary += `### 환경\n`;
230
+ summary += `- 브라우저: ${feedback.metadata.browser}\n`;
231
+ summary += `- OS: ${feedback.metadata.os}\n`;
232
+ summary += `- 화면: ${feedback.metadata.viewportSize}\n`;
233
+ if (feedback.metadata.userId) {
234
+ summary += `- 사용자: ${feedback.metadata.userName || feedback.metadata.userId}\n`;
235
+ }
236
+ }
237
+
238
+ return {
239
+ content: [
240
+ { type: 'text', text: summary },
241
+ { type: 'text', text: JSON.stringify(result, null, 2) },
242
+ ],
243
+ };
244
+ }
245
+
246
+ case 'update_feedback_status': {
247
+ const result = await api.updateFeedback(args.id, { status: args.status });
248
+ return {
249
+ content: [
250
+ {
251
+ type: 'text',
252
+ text: `피드백 상태가 '${args.status}'로 변경되었습니다.`,
253
+ },
254
+ ],
255
+ };
256
+ }
257
+
258
+ case 'add_comment': {
259
+ const result = await api.addComment(args.id, args.content);
260
+ return {
261
+ content: [
262
+ {
263
+ type: 'text',
264
+ text: `댓글이 추가되었습니다: "${args.content.slice(0, 50)}..."`,
265
+ },
266
+ ],
267
+ };
268
+ }
269
+
270
+ case 'get_stats': {
271
+ const result = await api.getStats();
272
+ return {
273
+ content: [
274
+ {
275
+ type: 'text',
276
+ text: JSON.stringify(result, null, 2),
277
+ },
278
+ ],
279
+ };
280
+ }
281
+
282
+ case 'analyze_feedback': {
283
+ const result = await api.getFeedback(args.id);
284
+ const feedback = result.data;
285
+
286
+ let analysis = `# 피드백 분석: ${feedback.title}\n\n`;
287
+
288
+ // 콘솔 로그 분석
289
+ const errors = (feedback.consoleLogs || []).filter(
290
+ (log) => log.level === 'error'
291
+ );
292
+ const warnings = (feedback.consoleLogs || []).filter(
293
+ (log) => log.level === 'warn'
294
+ );
295
+
296
+ analysis += `## 에러 분석\n\n`;
297
+
298
+ if (errors.length === 0 && warnings.length === 0) {
299
+ analysis += `콘솔에 에러/경고가 없습니다.\n\n`;
300
+ } else {
301
+ if (errors.length > 0) {
302
+ analysis += `### 에러 (${errors.length}개)\n`;
303
+ errors.forEach((err) => {
304
+ analysis += `\`\`\`\n${err.message}\n\`\`\`\n`;
305
+
306
+ // 파일/라인 추출 시도
307
+ const fileMatch = err.message.match(/at\s+.*?([\/\w-]+\.(js|ts|jsx|tsx)):(\d+)/);
308
+ if (fileMatch) {
309
+ analysis += `→ 파일: ${fileMatch[1]}:${fileMatch[3]}\n\n`;
310
+ }
311
+ });
312
+ }
313
+
314
+ if (warnings.length > 0) {
315
+ analysis += `### 경고 (${warnings.length}개)\n`;
316
+ warnings.slice(0, 5).forEach((warn) => {
317
+ analysis += `- ${warn.message.slice(0, 200)}\n`;
318
+ });
319
+ }
320
+ }
321
+
322
+ // 네트워크 에러 분석
323
+ if (feedback.networkErrors?.length > 0) {
324
+ analysis += `\n## 네트워크 에러\n\n`;
325
+ feedback.networkErrors.forEach((err) => {
326
+ analysis += `- \`${err.method} ${err.url}\` → ${err.status} ${err.statusText}\n`;
327
+ });
328
+ }
329
+
330
+ // URL 분석
331
+ if (feedback.url) {
332
+ const urlObj = new URL(feedback.url);
333
+ analysis += `\n## 페이지 정보\n\n`;
334
+ analysis += `- 경로: ${urlObj.pathname}\n`;
335
+ analysis += `- 쿼리: ${urlObj.search || '없음'}\n`;
336
+ analysis += `\n→ 관련 라우트 파일을 찾아보세요.\n`;
337
+ }
338
+
339
+ analysis += `\n## 권장 조치\n\n`;
340
+ analysis += `1. 위 에러 메시지에서 파일/라인 정보 확인\n`;
341
+ analysis += `2. 해당 코드 검색 및 수정\n`;
342
+ analysis += `3. 수정 완료 후 상태를 'resolved'로 변경\n`;
343
+
344
+ return {
345
+ content: [{ type: 'text', text: analysis }],
346
+ };
347
+ }
348
+
349
+ default:
350
+ return {
351
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
352
+ isError: true,
353
+ };
354
+ }
355
+ } catch (error) {
356
+ return {
357
+ content: [
358
+ {
359
+ type: 'text',
360
+ text: `Error: ${error.message}`,
361
+ },
362
+ ],
363
+ isError: true,
364
+ };
365
+ }
366
+ });
367
+
368
+ // 서버 시작
369
+ async function main() {
370
+ const transport = new StdioServerTransport();
371
+ await server.connect(transport);
372
+ console.error('Capman Feed MCP Server running');
373
+ }
374
+
375
+ main().catch((error) => {
376
+ console.error('Fatal error:', error);
377
+ process.exit(1);
378
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@nseed/capman-feed-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Capman Feed API - enables Claude Code and GitHub Copilot to access feedback data",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "capman-feed-mcp": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.0.0"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "claude",
20
+ "copilot",
21
+ "capman",
22
+ "feedback",
23
+ "bug-tracking"
24
+ ],
25
+ "author": "nseed <shin@nseed.co.kr>",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/nseed/capman-feed-mcp"
30
+ },
31
+ "homepage": "https://capman.kr/developers",
32
+ "bugs": {
33
+ "url": "https://github.com/nseed/capman-feed-mcp/issues"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ }
38
+ }