@mixio-pro/kalaasetu-mcp 2.0.11-beta → 2.1.1-beta
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/package.json +6 -2
- package/src/index.ts +4 -3
- package/src/storage/index.ts +4 -3
- package/src/tools/fal/config.ts +9 -8
- package/src/tools/fal/dynamic-tools.ts +214 -237
- package/src/tools/fal/models.ts +115 -93
- package/src/tools/fal/storage.ts +66 -61
- package/src/tools/gemini.ts +302 -281
- package/src/tools/get-status.ts +50 -46
- package/src/tools/image-to-video.ts +309 -300
- package/src/tools/perplexity.ts +188 -172
- package/src/tools/youtube.ts +45 -41
- package/src/utils/llm-prompt-enhancer.ts +3 -2
- package/src/utils/logger.ts +71 -0
- package/src/utils/openmeter.ts +123 -0
- package/src/utils/prompt-enhancer-presets.ts +7 -5
- package/src/utils/remote-sync.ts +19 -10
- package/src/utils/tool-credits.ts +104 -0
- package/src/utils/tool-wrapper.ts +37 -6
- package/src/utils/url-file.ts +4 -3
- package/src/test-context.ts +0 -52
- package/src/test-error-handling.ts +0 -31
- package/src/tools/image-to-video.sdk-backup.ts +0 -218
package/src/tools/perplexity.ts
CHANGED
|
@@ -12,13 +12,13 @@ export const perplexityImages = {
|
|
|
12
12
|
query: z
|
|
13
13
|
.string()
|
|
14
14
|
.describe(
|
|
15
|
-
"Descriptive search terms (e.g., 'SpaceX Starship launch photos')."
|
|
15
|
+
"Descriptive search terms (e.g., 'SpaceX Starship launch photos').",
|
|
16
16
|
),
|
|
17
17
|
image_domain_filter: z
|
|
18
18
|
.array(z.string())
|
|
19
19
|
.optional()
|
|
20
20
|
.describe(
|
|
21
|
-
"Filter results by domain. Use 'domain.com' to include, or '-domain.com' to exclude. (e.g., ['wikimedia.org', '-pinterest.com'])."
|
|
21
|
+
"Filter results by domain. Use 'domain.com' to include, or '-domain.com' to exclude. (e.g., ['wikimedia.org', '-pinterest.com']).",
|
|
22
22
|
),
|
|
23
23
|
image_format_filter: z
|
|
24
24
|
.array(z.string())
|
|
@@ -31,94 +31,102 @@ export const perplexityImages = {
|
|
|
31
31
|
image_domain_filter?: string[];
|
|
32
32
|
image_format_filter?: string[];
|
|
33
33
|
}) => {
|
|
34
|
-
return safeToolExecute(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const headers: Record<string, string> = {
|
|
42
|
-
Authorization: `Bearer ${apiKey}`,
|
|
43
|
-
"Content-Type": "application/json",
|
|
44
|
-
accept: "application/json",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const payload = {
|
|
48
|
-
model: "sonar",
|
|
49
|
-
messages: [
|
|
50
|
-
{ role: "user", content: `Show me images of ${args.query}` },
|
|
51
|
-
],
|
|
52
|
-
return_images: true,
|
|
53
|
-
...(args.image_domain_filter
|
|
54
|
-
? { image_domain_filter: args.image_domain_filter }
|
|
55
|
-
: {}),
|
|
56
|
-
...(args.image_format_filter
|
|
57
|
-
? { image_format_filter: args.image_format_filter }
|
|
58
|
-
: {}),
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const res = await fetch(url, {
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: headers,
|
|
64
|
-
body: JSON.stringify(payload),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const text = await res.text();
|
|
69
|
-
throw new Error(`Perplexity API request failed: ${res.status} ${text}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const data = (await res.json()) as any;
|
|
73
|
-
let content = data.choices?.[0]?.message?.content;
|
|
74
|
-
const images = (data.images || []) as any[];
|
|
75
|
-
const citations = (data.citations || []) as string[];
|
|
76
|
-
|
|
77
|
-
if (images.length === 0) {
|
|
78
|
-
return `No direct image URLs found in the API response. The text content was: ${content}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Create a map of origin_url -> new 1-based index
|
|
82
|
-
const originUrlToImageIndex: Record<string, number> = {};
|
|
83
|
-
images.forEach((img, index) => {
|
|
84
|
-
if (img.origin_url) {
|
|
85
|
-
originUrlToImageIndex[img.origin_url] = index + 1;
|
|
34
|
+
return safeToolExecute(
|
|
35
|
+
async () => {
|
|
36
|
+
const apiKey = process.env.PERPLEXITY_API_KEY;
|
|
37
|
+
if (!apiKey) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"PERPLEXITY_API_KEY environment variable is not set.",
|
|
40
|
+
);
|
|
86
41
|
}
|
|
87
|
-
});
|
|
88
42
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
43
|
+
const url = "https://api.perplexity.ai/chat/completions";
|
|
44
|
+
const headers: Record<string, string> = {
|
|
45
|
+
Authorization: `Bearer ${apiKey}`,
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
accept: "application/json",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const payload = {
|
|
51
|
+
model: "sonar",
|
|
52
|
+
messages: [
|
|
53
|
+
{ role: "user", content: `Show me images of ${args.query}` },
|
|
54
|
+
],
|
|
55
|
+
return_images: true,
|
|
56
|
+
...(args.image_domain_filter
|
|
57
|
+
? { image_domain_filter: args.image_domain_filter }
|
|
58
|
+
: {}),
|
|
59
|
+
...(args.image_format_filter
|
|
60
|
+
? { image_format_filter: args.image_format_filter }
|
|
61
|
+
: {}),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: headers,
|
|
67
|
+
body: JSON.stringify(payload),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const text = await res.text();
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Perplexity API request failed: ${res.status} ${text}`,
|
|
74
|
+
);
|
|
94
75
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
76
|
+
|
|
77
|
+
const data = (await res.json()) as any;
|
|
78
|
+
let content = data.choices?.[0]?.message?.content;
|
|
79
|
+
const images = (data.images || []) as any[];
|
|
80
|
+
const citations = (data.citations || []) as string[];
|
|
81
|
+
|
|
82
|
+
if (images.length === 0) {
|
|
83
|
+
return `No direct image URLs found in the API response. The text content was: ${content}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create a map of origin_url -> new 1-based index
|
|
87
|
+
const originUrlToImageIndex: Record<string, number> = {};
|
|
88
|
+
images.forEach((img, index) => {
|
|
89
|
+
if (img.origin_url) {
|
|
90
|
+
originUrlToImageIndex[img.origin_url] = index + 1;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Create a map of old citation index -> new image index
|
|
95
|
+
const oldToNewCitationMap: Record<number, number> = {};
|
|
96
|
+
citations.forEach((citationUrl, index) => {
|
|
97
|
+
if (originUrlToImageIndex[citationUrl]) {
|
|
98
|
+
oldToNewCitationMap[index + 1] = originUrlToImageIndex[citationUrl];
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Replace citations in the content
|
|
103
|
+
if (content && typeof content === "string") {
|
|
104
|
+
content = content
|
|
105
|
+
.replace(/\[(\d+)\]/g, (_match, oldIndexStr) => {
|
|
106
|
+
const oldIndex = parseInt(oldIndexStr, 10);
|
|
107
|
+
const newIndex = oldToNewCitationMap[oldIndex];
|
|
108
|
+
if (newIndex) {
|
|
109
|
+
return `[${newIndex}]`;
|
|
110
|
+
}
|
|
111
|
+
return "";
|
|
112
|
+
})
|
|
113
|
+
.replace(/(\s\s+)/g, " ")
|
|
114
|
+
.trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Build the final formatted output
|
|
118
|
+
let output = content + "\n\n--- Images ---\n";
|
|
119
|
+
images.forEach((img, index) => {
|
|
120
|
+
output += `${index + 1}. ${img.image_url}\n (Source: ${
|
|
121
|
+
img.origin_url
|
|
122
|
+
})\n`;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return output;
|
|
126
|
+
},
|
|
127
|
+
"perplexityImages",
|
|
128
|
+
{ toolName: "perplexityImages" },
|
|
129
|
+
);
|
|
122
130
|
},
|
|
123
131
|
};
|
|
124
132
|
|
|
@@ -134,97 +142,105 @@ export const perplexityVideos = {
|
|
|
134
142
|
.array(z.string())
|
|
135
143
|
.optional()
|
|
136
144
|
.describe(
|
|
137
|
-
"Optional: Restrict search to specific domains (e.g., ['youtube.com']) or exclude them with '-' prefix."
|
|
145
|
+
"Optional: Restrict search to specific domains (e.g., ['youtube.com']) or exclude them with '-' prefix.",
|
|
138
146
|
),
|
|
139
147
|
}),
|
|
140
148
|
timeoutMs: 300000,
|
|
141
149
|
execute: async (args: { query: string; search_domain_filter?: string[] }) => {
|
|
142
|
-
return safeToolExecute(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const headers: Record<string, string> = {
|
|
150
|
-
Authorization: `Bearer ${apiKey}`,
|
|
151
|
-
"Content-Type": "application/json",
|
|
152
|
-
accept: "application/json",
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const payload = {
|
|
156
|
-
model: "sonar-pro",
|
|
157
|
-
messages: [
|
|
158
|
-
{ role: "user", content: `Show me videos of ${args.query}` },
|
|
159
|
-
],
|
|
160
|
-
media_response: { overrides: { return_videos: true } },
|
|
161
|
-
...(args.search_domain_filter
|
|
162
|
-
? { search_domain_filter: args.search_domain_filter }
|
|
163
|
-
: {}),
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const res = await fetch(url, {
|
|
167
|
-
method: "POST",
|
|
168
|
-
headers: headers,
|
|
169
|
-
body: JSON.stringify(payload),
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (!res.ok) {
|
|
173
|
-
const text = await res.text();
|
|
174
|
-
throw new Error(`Perplexity API request failed: ${res.status} ${text}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const data = (await res.json()) as any;
|
|
178
|
-
let content = data.choices?.[0]?.message?.content;
|
|
179
|
-
const videos = (data.videos || []) as any[];
|
|
180
|
-
const citations = (data.citations || []) as string[];
|
|
181
|
-
|
|
182
|
-
if (videos.length === 0) {
|
|
183
|
-
return `No direct video URLs found in the API response. Full API Response: ${JSON.stringify(
|
|
184
|
-
data,
|
|
185
|
-
null,
|
|
186
|
-
2
|
|
187
|
-
)}`;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Create a map of video url -> new 1-based index
|
|
191
|
-
const urlToVideoIndex: Record<string, number> = {};
|
|
192
|
-
videos.forEach((video, index) => {
|
|
193
|
-
if (video.url) {
|
|
194
|
-
urlToVideoIndex[video.url] = index + 1;
|
|
150
|
+
return safeToolExecute(
|
|
151
|
+
async () => {
|
|
152
|
+
const apiKey = process.env.PERPLEXITY_API_KEY;
|
|
153
|
+
if (!apiKey) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"PERPLEXITY_API_KEY environment variable is not set.",
|
|
156
|
+
);
|
|
195
157
|
}
|
|
196
|
-
});
|
|
197
158
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
159
|
+
const url = "https://api.perplexity.ai/chat/completions";
|
|
160
|
+
const headers: Record<string, string> = {
|
|
161
|
+
Authorization: `Bearer ${apiKey}`,
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
accept: "application/json",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const payload = {
|
|
167
|
+
model: "sonar-pro",
|
|
168
|
+
messages: [
|
|
169
|
+
{ role: "user", content: `Show me videos of ${args.query}` },
|
|
170
|
+
],
|
|
171
|
+
media_response: { overrides: { return_videos: true } },
|
|
172
|
+
...(args.search_domain_filter
|
|
173
|
+
? { search_domain_filter: args.search_domain_filter }
|
|
174
|
+
: {}),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const res = await fetch(url, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: headers,
|
|
180
|
+
body: JSON.stringify(payload),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
const text = await res.text();
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Perplexity API request failed: ${res.status} ${text}`,
|
|
187
|
+
);
|
|
203
188
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
189
|
+
|
|
190
|
+
const data = (await res.json()) as any;
|
|
191
|
+
let content = data.choices?.[0]?.message?.content;
|
|
192
|
+
const videos = (data.videos || []) as any[];
|
|
193
|
+
const citations = (data.citations || []) as string[];
|
|
194
|
+
|
|
195
|
+
if (videos.length === 0) {
|
|
196
|
+
return `No direct video URLs found in the API response. Full API Response: ${JSON.stringify(
|
|
197
|
+
data,
|
|
198
|
+
null,
|
|
199
|
+
2,
|
|
200
|
+
)}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Create a map of video url -> new 1-based index
|
|
204
|
+
const urlToVideoIndex: Record<string, number> = {};
|
|
205
|
+
videos.forEach((video, index) => {
|
|
206
|
+
if (video.url) {
|
|
207
|
+
urlToVideoIndex[video.url] = index + 1;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Create a map of old citation index -> new video index
|
|
212
|
+
const oldToNewCitationMap: Record<number, number> = {};
|
|
213
|
+
citations.forEach((citationUrl, index) => {
|
|
214
|
+
if (urlToVideoIndex[citationUrl]) {
|
|
215
|
+
oldToNewCitationMap[index + 1] = urlToVideoIndex[citationUrl];
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Replace citations in the content
|
|
220
|
+
if (content && typeof content === "string") {
|
|
221
|
+
content = content
|
|
222
|
+
.replace(/\[(\d+)\]/g, (_match, oldIndexStr) => {
|
|
223
|
+
const oldIndex = parseInt(oldIndexStr, 10);
|
|
224
|
+
const newIndex = oldToNewCitationMap[oldIndex];
|
|
225
|
+
if (newIndex) {
|
|
226
|
+
return `[${newIndex}]`;
|
|
227
|
+
}
|
|
228
|
+
return "";
|
|
229
|
+
})
|
|
230
|
+
.replace(/(\s\s+)/g, " ")
|
|
231
|
+
.trim();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Build the final formatted output
|
|
235
|
+
let output = content + "\n\n--- Videos ---\n";
|
|
236
|
+
videos.forEach((video, index) => {
|
|
237
|
+
output += `${index + 1}. ${video.url}\n`;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return output;
|
|
241
|
+
},
|
|
242
|
+
"perplexityVideos",
|
|
243
|
+
{ toolName: "perplexityVideos" },
|
|
244
|
+
);
|
|
229
245
|
},
|
|
230
246
|
};
|
package/src/tools/youtube.ts
CHANGED
|
@@ -17,60 +17,64 @@ export const analyzeYoutubeVideo = {
|
|
|
17
17
|
youtube_url: z
|
|
18
18
|
.string()
|
|
19
19
|
.describe(
|
|
20
|
-
"The full URL of the YouTube video (e.g., 'https://www.youtube.com/watch?v=dQw4w9WgXcQ')."
|
|
20
|
+
"The full URL of the YouTube video (e.g., 'https://www.youtube.com/watch?v=dQw4w9WgXcQ').",
|
|
21
21
|
),
|
|
22
22
|
prompt: z
|
|
23
23
|
.string()
|
|
24
24
|
.describe(
|
|
25
|
-
"Instruction or question about the video content (e.g., 'Summarize the main points' or 'What color was the car?')."
|
|
25
|
+
"Instruction or question about the video content (e.g., 'Summarize the main points' or 'What color was the car?').",
|
|
26
26
|
),
|
|
27
27
|
}),
|
|
28
28
|
timeoutMs: 300000,
|
|
29
29
|
execute: async (args: { youtube_url: string; prompt: string }) => {
|
|
30
|
-
return safeToolExecute(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
!args.youtube_url.includes("
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
return safeToolExecute(
|
|
31
|
+
async () => {
|
|
32
|
+
try {
|
|
33
|
+
// Validate YouTube URL format
|
|
34
|
+
if (
|
|
35
|
+
!args.youtube_url ||
|
|
36
|
+
(!args.youtube_url.includes("youtube.com/watch") &&
|
|
37
|
+
!args.youtube_url.includes("youtu.be"))
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Invalid YouTube URL format. Expected: https://www.youtube.com/watch?v=VIDEO_ID",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
// Create content using the correct FileData approach with fileUri
|
|
45
|
+
const response = await ai.models.generateContent({
|
|
46
|
+
model: "models/gemini-2.5-flash",
|
|
47
|
+
contents: {
|
|
48
|
+
parts: [
|
|
49
|
+
{
|
|
50
|
+
fileData: {
|
|
51
|
+
fileUri: args.youtube_url,
|
|
52
|
+
},
|
|
51
53
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
});
|
|
54
|
+
{ text: args.prompt },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
let result = "";
|
|
60
|
+
if (response.candidates && response.candidates[0]?.content?.parts) {
|
|
61
|
+
for (const part of response.candidates[0].content.parts) {
|
|
62
|
+
if (part.text) {
|
|
63
|
+
result += part.text;
|
|
64
|
+
}
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
|
-
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
return (
|
|
69
|
+
result ||
|
|
70
|
+
"YouTube video analysis completed but no text response received"
|
|
71
|
+
);
|
|
72
|
+
} catch (error: any) {
|
|
73
|
+
throw new Error(`YouTube video analysis failed: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"analyzeYoutubeVideo",
|
|
77
|
+
{ toolName: "analyzeYoutubeVideo" },
|
|
78
|
+
);
|
|
75
79
|
},
|
|
76
80
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { GoogleGenAI } from "@google/genai";
|
|
9
|
+
import { logger } from "./logger";
|
|
9
10
|
|
|
10
11
|
const ai = new GoogleGenAI({
|
|
11
12
|
apiKey: process.env.GEMINI_API_KEY || "",
|
|
@@ -236,7 +237,7 @@ export const LLM_ENHANCER_CONFIGS: Record<string, LLMEnhancerConfig> = {
|
|
|
236
237
|
export async function enhancePromptWithLLM(
|
|
237
238
|
prompt: string,
|
|
238
239
|
configOrName: string | LLMEnhancerConfig = "ltx2",
|
|
239
|
-
images?: string[]
|
|
240
|
+
images?: string[],
|
|
240
241
|
): Promise<string> {
|
|
241
242
|
// Resolve config - ltx2 is always available as default
|
|
242
243
|
let config: LLMEnhancerConfig;
|
|
@@ -288,7 +289,7 @@ export async function enhancePromptWithLLM(
|
|
|
288
289
|
|
|
289
290
|
return enhancedPrompt;
|
|
290
291
|
} catch (error: any) {
|
|
291
|
-
|
|
292
|
+
logger.error(`LLM prompt enhancement failed: ${error.message}`);
|
|
292
293
|
// Fall back to original prompt on error
|
|
293
294
|
return prompt;
|
|
294
295
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Winston + Sentry logging for MCP server.
|
|
3
|
+
* All logs go to stderr (MCP-safe). Errors are also sent to Sentry if configured.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import winston from "winston";
|
|
7
|
+
import Sentry from "winston-transport-sentry-node";
|
|
8
|
+
|
|
9
|
+
const { combine, timestamp, printf, colorize } = winston.format;
|
|
10
|
+
|
|
11
|
+
// Default Sentry DSN - can be overridden via SENTRY_DSN env var
|
|
12
|
+
const DEFAULT_SENTRY_DSN =
|
|
13
|
+
"https://349c280f4b1c731bc1ac1a1189cae5e7@o4510770034245632.ingest.us.sentry.io/4510770047025152";
|
|
14
|
+
|
|
15
|
+
// Custom format for console output (cleaner for MCP inspector)
|
|
16
|
+
const consoleFormat = printf(({ level, message, timestamp, tags }) => {
|
|
17
|
+
const clientId = tags?.client_id ? ` [${tags.client_id}]` : "";
|
|
18
|
+
return `${timestamp} [${level}]${clientId} ${message}`;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Determine log level from environment
|
|
22
|
+
const LOG_LEVEL = process.env.LOG_LEVEL || "info";
|
|
23
|
+
|
|
24
|
+
// Create transports array
|
|
25
|
+
const transports: winston.transport[] = [
|
|
26
|
+
// Console transport - ALL levels go to stderr (MCP-safe)
|
|
27
|
+
new winston.transports.Console({
|
|
28
|
+
stderrLevels: [
|
|
29
|
+
"error",
|
|
30
|
+
"warn",
|
|
31
|
+
"info",
|
|
32
|
+
"http",
|
|
33
|
+
"verbose",
|
|
34
|
+
"debug",
|
|
35
|
+
"silly",
|
|
36
|
+
],
|
|
37
|
+
format: combine(
|
|
38
|
+
colorize(),
|
|
39
|
+
timestamp({ format: "HH:mm:ss" }),
|
|
40
|
+
consoleFormat,
|
|
41
|
+
),
|
|
42
|
+
}),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Add Sentry transport - uses default DSN if not overridden
|
|
46
|
+
const SENTRY_DSN = process.env.SENTRY_DSN || DEFAULT_SENTRY_DSN;
|
|
47
|
+
transports.push(
|
|
48
|
+
new Sentry({
|
|
49
|
+
sentry: {
|
|
50
|
+
dsn: SENTRY_DSN,
|
|
51
|
+
environment: process.env.NODE_ENV || "development",
|
|
52
|
+
},
|
|
53
|
+
level: "error", // Only send errors and above to Sentry
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Create the logger instance
|
|
58
|
+
export const logger = winston.createLogger({
|
|
59
|
+
level: LOG_LEVEL,
|
|
60
|
+
format: combine(timestamp(), winston.format.json()),
|
|
61
|
+
defaultMeta: {
|
|
62
|
+
service: "kalaasetu-mcp",
|
|
63
|
+
tags: {
|
|
64
|
+
client_id: process.env.CLIENT_ID || "unknown",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
transports,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Log startup info
|
|
71
|
+
logger.info("Sentry error tracking enabled");
|