@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.
@@ -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(async () => {
35
- const apiKey = process.env.PERPLEXITY_API_KEY;
36
- if (!apiKey) {
37
- throw new Error("PERPLEXITY_API_KEY environment variable is not set.");
38
- }
39
-
40
- const url = "https://api.perplexity.ai/chat/completions";
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
- // Create a map of old citation index -> new image index
90
- const oldToNewCitationMap: Record<number, number> = {};
91
- citations.forEach((citationUrl, index) => {
92
- if (originUrlToImageIndex[citationUrl]) {
93
- oldToNewCitationMap[index + 1] = originUrlToImageIndex[citationUrl];
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
- // Replace citations in the content
98
- if (content && typeof content === "string") {
99
- content = content
100
- .replace(/\[(\d+)\]/g, (_match, oldIndexStr) => {
101
- const oldIndex = parseInt(oldIndexStr, 10);
102
- const newIndex = oldToNewCitationMap[oldIndex];
103
- if (newIndex) {
104
- return `[${newIndex}]`;
105
- }
106
- return "";
107
- })
108
- .replace(/(\s\s+)/g, " ")
109
- .trim();
110
- }
111
-
112
- // Build the final formatted output
113
- let output = content + "\n\n--- Images ---\n";
114
- images.forEach((img, index) => {
115
- output += `${index + 1}. ${img.image_url}\n (Source: ${
116
- img.origin_url
117
- })\n`;
118
- });
119
-
120
- return output;
121
- }, "perplexityImages");
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(async () => {
143
- const apiKey = process.env.PERPLEXITY_API_KEY;
144
- if (!apiKey) {
145
- throw new Error("PERPLEXITY_API_KEY environment variable is not set.");
146
- }
147
-
148
- const url = "https://api.perplexity.ai/chat/completions";
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
- // Create a map of old citation index -> new video index
199
- const oldToNewCitationMap: Record<number, number> = {};
200
- citations.forEach((citationUrl, index) => {
201
- if (urlToVideoIndex[citationUrl]) {
202
- oldToNewCitationMap[index + 1] = urlToVideoIndex[citationUrl];
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
- // Replace citations in the content
207
- if (content && typeof content === "string") {
208
- content = content
209
- .replace(/\[(\d+)\]/g, (_match, oldIndexStr) => {
210
- const oldIndex = parseInt(oldIndexStr, 10);
211
- const newIndex = oldToNewCitationMap[oldIndex];
212
- if (newIndex) {
213
- return `[${newIndex}]`;
214
- }
215
- return "";
216
- })
217
- .replace(/(\s\s+)/g, " ")
218
- .trim();
219
- }
220
-
221
- // Build the final formatted output
222
- let output = content + "\n\n--- Videos ---\n";
223
- videos.forEach((video, index) => {
224
- output += `${index + 1}. ${video.url}\n`;
225
- });
226
-
227
- return output;
228
- }, "perplexityVideos");
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
  };
@@ -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(async () => {
31
- try {
32
- // Validate YouTube URL format
33
- if (
34
- !args.youtube_url ||
35
- (!args.youtube_url.includes("youtube.com/watch") &&
36
- !args.youtube_url.includes("youtu.be"))
37
- ) {
38
- throw new Error(
39
- "Invalid YouTube URL format. Expected: https://www.youtube.com/watch?v=VIDEO_ID"
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
- // Create content using the correct FileData approach with fileUri
44
- const response = await ai.models.generateContent({
45
- model: "models/gemini-2.5-flash",
46
- contents: {
47
- parts: [
48
- {
49
- fileData: {
50
- fileUri: args.youtube_url,
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
- { text: args.prompt },
54
- ],
55
- },
56
- });
54
+ { text: args.prompt },
55
+ ],
56
+ },
57
+ });
57
58
 
58
- let result = "";
59
- if (response.candidates && response.candidates[0]?.content?.parts) {
60
- for (const part of response.candidates[0].content.parts) {
61
- if (part.text) {
62
- result += part.text;
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
- return (
68
- result ||
69
- "YouTube video analysis completed but no text response received"
70
- );
71
- } catch (error: any) {
72
- throw new Error(`YouTube video analysis failed: ${error.message}`);
73
- }
74
- }, "analyzeYoutubeVideo");
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
- console.error(`LLM prompt enhancement failed: ${error.message}`);
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");