@sophia-vibelog/mcp-server 0.3.1 → 0.4.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.
- package/dist/index.js +84 -38
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,29 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
|
|
6
|
+
// ── 버전 (package.json에서 동적 로드) ──
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version: PKG_VERSION } = require("../package.json");
|
|
9
|
+
// ── 프로세스 레벨 에러 핸들러 (복구 없이 로깅 + 종료) ──
|
|
10
|
+
process.on("uncaughtException", (err) => {
|
|
11
|
+
console.error("[sophia-mcp] uncaughtException:", err.message);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
14
|
+
process.on("unhandledRejection", (reason) => {
|
|
15
|
+
console.error("[sophia-mcp] unhandledRejection:", reason);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
18
|
+
// ── env 검증 (P0: 미설정 시 즉시 실패) ──
|
|
19
|
+
const SOPHIA_API_URL = process.env.SOPHIA_API_URL || "";
|
|
6
20
|
const SOPHIA_API_KEY = process.env.SOPHIA_API_KEY || "";
|
|
21
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
22
|
+
if (!SOPHIA_API_URL) {
|
|
23
|
+
console.error("[sophia-mcp] SOPHIA_API_URL이 설정되지 않았습니다. MCP config의 env에 설정해주세요.");
|
|
24
|
+
}
|
|
25
|
+
if (!SOPHIA_API_KEY) {
|
|
26
|
+
console.error("[sophia-mcp] SOPHIA_API_KEY가 설정되지 않았습니다. MCP config의 env에 설정해주세요.");
|
|
27
|
+
}
|
|
28
|
+
/** env 미설정 시 도구 호출에서 반환할 에러 */
|
|
29
|
+
function envError() {
|
|
30
|
+
const missing = [];
|
|
31
|
+
if (!SOPHIA_API_URL)
|
|
32
|
+
missing.push("SOPHIA_API_URL");
|
|
33
|
+
if (!SOPHIA_API_KEY)
|
|
34
|
+
missing.push("SOPHIA_API_KEY");
|
|
35
|
+
if (missing.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: `환경변수가 설정되지 않았습니다: ${missing.join(", ")}.\nMCP config의 env 블록에 추가해주세요.\n예: "env": { "SOPHIA_API_KEY": "sk-...", "SOPHIA_API_URL": "https://..." }`,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
7
47
|
const server = new McpServer({
|
|
8
48
|
name: "sophia-mcp",
|
|
9
|
-
version:
|
|
49
|
+
version: PKG_VERSION,
|
|
10
50
|
});
|
|
11
51
|
server.tool("publish-log", "VibeLog에 AI 코딩 세션 로그를 발행합니다. 세션 로그 텍스트를 전달하면 AI가 분석하고 소피아가 조언을 남깁니다.", {
|
|
12
52
|
rawSessionLog: z.string().describe("AI 코딩 세션의 전체 로그 텍스트"),
|
|
13
53
|
title: z.string().optional().describe("로그 제목 (선택)"),
|
|
14
54
|
projectId: z.string().optional().describe("프로젝트 ID (선택)"),
|
|
15
55
|
}, async ({ rawSessionLog, title, projectId }) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
type: "text",
|
|
21
|
-
text: "SOPHIA_API_KEY가 설정되지 않았습니다. VibeLog 설정에서 API 키를 생성한 후 환경변수에 설정해주세요.",
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
isError: true,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
56
|
+
const env = envError();
|
|
57
|
+
if (env)
|
|
58
|
+
return env;
|
|
27
59
|
try {
|
|
28
60
|
const response = await fetch(`${SOPHIA_API_URL}/api/logs`, {
|
|
29
61
|
method: "POST",
|
|
@@ -36,6 +68,7 @@ server.tool("publish-log", "VibeLog에 AI 코딩 세션 로그를 발행합니
|
|
|
36
68
|
title: title || undefined,
|
|
37
69
|
projectId: projectId || undefined,
|
|
38
70
|
}),
|
|
71
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
39
72
|
});
|
|
40
73
|
if (!response.ok) {
|
|
41
74
|
const error = await response.json().catch(() => ({}));
|
|
@@ -63,35 +96,27 @@ server.tool("publish-log", "VibeLog에 AI 코딩 세션 로그를 발행합니
|
|
|
63
96
|
};
|
|
64
97
|
}
|
|
65
98
|
catch (e) {
|
|
99
|
+
const isTimeout = e instanceof DOMException && e.name === "TimeoutError";
|
|
100
|
+
const msg = isTimeout
|
|
101
|
+
? `VibeLog 서버 응답 없음 (${FETCH_TIMEOUT_MS / 1000}초 타임아웃). 서버 상태를 확인해주세요: ${SOPHIA_API_URL}`
|
|
102
|
+
: `VibeLog 서버 연결 실패: ${e instanceof Error ? e.message : "알 수 없는 오류"}. SOPHIA_API_URL을 확인해주세요.`;
|
|
103
|
+
console.error(`[sophia-mcp] publish-log: ${msg}`);
|
|
66
104
|
return {
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: "text",
|
|
70
|
-
text: `VibeLog 서버 연결 실패: ${e instanceof Error ? e.message : "알 수 없는 오류"}. SOPHIA_API_URL을 확인해주세요.`,
|
|
71
|
-
},
|
|
72
|
-
],
|
|
105
|
+
content: [{ type: "text", text: msg }],
|
|
73
106
|
isError: true,
|
|
74
107
|
};
|
|
75
108
|
}
|
|
76
109
|
});
|
|
77
|
-
server.tool("rate-prompt", "사용자의 질문 품질을 평가하고 개선 제안을 제공합니다. 질문 끝에 @@가 붙어있을 때만 사용하세요. 중요: 이
|
|
110
|
+
server.tool("rate-prompt", "사용자의 질문 품질을 평가하고 개선 제안을 제공합니다. 질문 끝에 @@가 붙어있을 때만 사용하세요. 중요: 이 도구를 호출한 후, 반환된 텍스트를 반드시 당신의 응답 본문 텍스트에 직접 포함시켜 출력하세요. 도구 결과 블록에만 남겨두면 사용자가 볼 수 없습니다. 응답의 가장 마지막에 --- 구분선 아래, 원본 포맷 그대로(━━━ 테두리, 들여쓰기, 이모지 포함) 복사해서 출력하세요. 재가공, 요약, 마크다운 변환을 하지 마세요.", {
|
|
78
111
|
userMessage: z.string().describe("평가할 사용자 메시지 (@@는 제거된 상태)"),
|
|
79
112
|
conversationContext: z
|
|
80
113
|
.string()
|
|
81
114
|
.optional()
|
|
82
115
|
.describe("대화 맥락 (선택사항)"),
|
|
83
116
|
}, async ({ userMessage, conversationContext }) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{
|
|
88
|
-
type: "text",
|
|
89
|
-
text: "SOPHIA_API_KEY가 설정되지 않았습니다. VibeLog 설정에서 API 키를 생성한 후 환경변수에 설정해주세요.",
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
isError: true,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
117
|
+
const env = envError();
|
|
118
|
+
if (env)
|
|
119
|
+
return env;
|
|
95
120
|
try {
|
|
96
121
|
const response = await fetch(`${SOPHIA_API_URL}/api/rate-prompt`, {
|
|
97
122
|
method: "POST",
|
|
@@ -103,6 +128,7 @@ server.tool("rate-prompt", "사용자의 질문 품질을 평가하고 개선
|
|
|
103
128
|
userMessage,
|
|
104
129
|
conversationContext: conversationContext || undefined,
|
|
105
130
|
}),
|
|
131
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
106
132
|
});
|
|
107
133
|
if (!response.ok) {
|
|
108
134
|
const error = await response.json().catch(() => ({}));
|
|
@@ -116,7 +142,27 @@ server.tool("rate-prompt", "사용자의 질문 품질을 평가하고 개선
|
|
|
116
142
|
isError: true,
|
|
117
143
|
};
|
|
118
144
|
}
|
|
119
|
-
const
|
|
145
|
+
const CoachingResultSchema = z.object({
|
|
146
|
+
encouragement: z.string(),
|
|
147
|
+
strengths: z.array(z.string()),
|
|
148
|
+
improvements: z.array(z.string()),
|
|
149
|
+
example: z.string(),
|
|
150
|
+
});
|
|
151
|
+
const raw = await response.json();
|
|
152
|
+
const parsed = CoachingResultSchema.safeParse(raw);
|
|
153
|
+
if (!parsed.success) {
|
|
154
|
+
console.error("[sophia-mcp] rate-prompt: 응답 스키마 불일치:", parsed.error.message);
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: "프롬프트 평가 결과를 처리할 수 없습니다. 서버 응답 형식이 예상과 다릅니다.",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
isError: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const result = parsed.data;
|
|
120
166
|
// 피드백 포맷팅
|
|
121
167
|
let feedback = `\n━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
122
168
|
feedback += `💬 소피아의 질문 코칭\n\n`;
|
|
@@ -145,13 +191,13 @@ server.tool("rate-prompt", "사용자의 질문 품질을 평가하고 개선
|
|
|
145
191
|
};
|
|
146
192
|
}
|
|
147
193
|
catch (e) {
|
|
194
|
+
const isTimeout = e instanceof DOMException && e.name === "TimeoutError";
|
|
195
|
+
const msg = isTimeout
|
|
196
|
+
? `프롬프트 평가 서버 응답 없음 (${FETCH_TIMEOUT_MS / 1000}초 타임아웃). 서버 상태를 확인해주세요: ${SOPHIA_API_URL}`
|
|
197
|
+
: `프롬프트 평가 실패: ${e instanceof Error ? e.message : "알 수 없는 오류"}`;
|
|
198
|
+
console.error(`[sophia-mcp] rate-prompt: ${msg}`);
|
|
148
199
|
return {
|
|
149
|
-
content: [
|
|
150
|
-
{
|
|
151
|
-
type: "text",
|
|
152
|
-
text: `프롬프트 평가 실패: ${e instanceof Error ? e.message : "알 수 없는 오류"}`,
|
|
153
|
-
},
|
|
154
|
-
],
|
|
200
|
+
content: [{ type: "text", text: msg }],
|
|
155
201
|
isError: true,
|
|
156
202
|
};
|
|
157
203
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sophia-vibelog/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Sophia (VibeLog) MCP Server — Claude Code에서 AI 코딩 세션 로그를 자동 발행합니다",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc",
|
|
28
28
|
"dev": "tsc --watch",
|
|
29
|
+
"test:smoke": "node test/smoke.mjs",
|
|
29
30
|
"prepublishOnly": "npm run build",
|
|
30
31
|
"lint": "echo 'no lint configured'"
|
|
31
32
|
},
|