ydc-mcp-server 1.6.1 → 1.7.8

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Anthropic/Claude Parameter Mapper Module
3
+ * Maps Anthropic API parameters to You.com API parameters
4
+ */
5
+
6
+ import {
7
+ getAdvancedVersion,
8
+ isAdvancedVersion,
9
+ getDefaultAdvancedVersion,
10
+ adjustWorkflowSteps
11
+ } from './advanced-versions.js';
12
+
13
+ /**
14
+ * Map Anthropic request parameters to You.com parameters
15
+ */
16
+ export function mapAnthropicToYouParams(anthropicRequest) {
17
+ const {
18
+ model = 'advanced-3.0-high',
19
+ messages,
20
+ system,
21
+ temperature = 0.7,
22
+ max_tokens = 1024,
23
+ stream = false
24
+ } = anthropicRequest;
25
+
26
+ let input = '';
27
+ const conversationHistory = [];
28
+
29
+ // Handle system prompt
30
+ if (system) {
31
+ input = `[System Instructions]\n${system}\n\n`;
32
+ }
33
+
34
+ // Process messages
35
+ messages.forEach(msg => {
36
+ if (msg.role === 'user') {
37
+ // Handle content as string or array
38
+ const content = typeof msg.content === 'string'
39
+ ? msg.content
40
+ : msg.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
41
+ conversationHistory.push(`User: ${content}`);
42
+ } else if (msg.role === 'assistant') {
43
+ const content = typeof msg.content === 'string'
44
+ ? msg.content
45
+ : msg.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
46
+ conversationHistory.push(`Assistant: ${content}`);
47
+ }
48
+ });
49
+
50
+ if (conversationHistory.length > 1) {
51
+ input += `[Conversation History]\n${conversationHistory.slice(0, -1).join('\n\n')}\n\n`;
52
+ input += `[Current Message]\n${conversationHistory[conversationHistory.length - 1].replace(/^User: /, '')}`;
53
+ } else if (conversationHistory.length === 1) {
54
+ input += conversationHistory[0].replace(/^User: /, '');
55
+ }
56
+
57
+ // Map model to You.com agent
58
+ const modelMapping = {
59
+ 'claude-3-opus-20240229': 'advanced-3.0-high',
60
+ 'claude-3-sonnet-20240229': 'advanced-3.0-medium',
61
+ 'claude-3-haiku-20240307': 'express',
62
+ 'claude-3-5-sonnet-20240620': 'advanced-3.0-high',
63
+ 'claude-3-5-sonnet-20241022': 'advanced-3.0-high',
64
+ 'claude-sonnet-4-20250514': 'advanced-3.0-high',
65
+ 'claude-sonnet-4-5-20250929': 'advanced-4.5-research'
66
+ };
67
+
68
+ const mappedModel = modelMapping[model] || model;
69
+
70
+ // Check if it's an advanced version model
71
+ if (isAdvancedVersion(mappedModel)) {
72
+ const versionConfig = getAdvancedVersion(mappedModel);
73
+ if (versionConfig) {
74
+ const adjustedSteps = adjustWorkflowSteps(versionConfig.max_workflow_steps, temperature);
75
+
76
+ return {
77
+ agent: 'advanced',
78
+ input,
79
+ stream,
80
+ verbosity: versionConfig.verbosity,
81
+ tools: versionConfig.tools,
82
+ workflow_config: {
83
+ max_workflow_steps: adjustedSteps
84
+ },
85
+ timeout: versionConfig.timeout
86
+ };
87
+ }
88
+ }
89
+
90
+ // Default handling
91
+ let agent = mappedModel;
92
+ let verbosity = 'medium';
93
+ let timeout = 300000;
94
+
95
+ if (temperature <= 0.3) verbosity = 'medium';
96
+ else if (temperature >= 0.8) verbosity = 'high';
97
+
98
+ const max_workflow_steps = Math.min(Math.max(Math.floor(max_tokens / 100), 1), 20);
99
+
100
+ const knownAgents = ['express', 'research', 'advanced'];
101
+ if (!knownAgents.includes(agent)) {
102
+ agent = 'advanced';
103
+ }
104
+
105
+ if (agent === 'advanced') {
106
+ timeout = 3000000;
107
+ }
108
+
109
+ return {
110
+ agent,
111
+ input,
112
+ stream,
113
+ verbosity,
114
+ tools: [
115
+ { type: 'research', search_effort: 'auto', report_verbosity: 'medium' },
116
+ { type: 'compute' }
117
+ ],
118
+ workflow_config: {
119
+ max_workflow_steps
120
+ },
121
+ timeout
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Convert You.com response to Anthropic format
127
+ */
128
+ export function convertToAnthropicResponse(youResponse, model, inputTokens = 100) {
129
+ const content = youResponse.output && Array.isArray(youResponse.output)
130
+ ? youResponse.output
131
+ .filter(item => item.type === 'message.answer')
132
+ .map(item => item.text)
133
+ .join('\n\n')
134
+ : 'No response content';
135
+
136
+ const outputTokens = Math.floor(content.length / 4);
137
+
138
+ return {
139
+ id: `msg_${Date.now()}`,
140
+ type: 'message',
141
+ role: 'assistant',
142
+ content: [
143
+ {
144
+ type: 'text',
145
+ text: content
146
+ }
147
+ ],
148
+ model: model,
149
+ stop_reason: 'end_turn',
150
+ stop_sequence: null,
151
+ usage: {
152
+ input_tokens: inputTokens,
153
+ output_tokens: outputTokens
154
+ }
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Create streaming event in Anthropic format
160
+ */
161
+ export function createAnthropicStreamEvent(eventType, data) {
162
+ return `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
163
+ }
164
+
165
+ /**
166
+ * Create message_start event
167
+ */
168
+ export function createMessageStartEvent(model, conversationId = null) {
169
+ const message = {
170
+ id: `msg_${Date.now()}`,
171
+ type: 'message',
172
+ role: 'assistant',
173
+ content: [],
174
+ model: model,
175
+ stop_reason: null,
176
+ stop_sequence: null,
177
+ usage: {
178
+ input_tokens: 0,
179
+ output_tokens: 0
180
+ }
181
+ };
182
+
183
+ // Add conversation_id in metadata if provided
184
+ if (conversationId) {
185
+ message.metadata = { conversation_id: conversationId };
186
+ }
187
+
188
+ return createAnthropicStreamEvent('message_start', {
189
+ type: 'message_start',
190
+ message
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Create content_block_start event
196
+ */
197
+ export function createContentBlockStartEvent(index = 0) {
198
+ return createAnthropicStreamEvent('content_block_start', {
199
+ type: 'content_block_start',
200
+ index: index,
201
+ content_block: {
202
+ type: 'text',
203
+ text: ''
204
+ }
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Create content_block_delta event
210
+ */
211
+ export function createContentBlockDeltaEvent(text, index = 0) {
212
+ return createAnthropicStreamEvent('content_block_delta', {
213
+ type: 'content_block_delta',
214
+ index: index,
215
+ delta: {
216
+ type: 'text_delta',
217
+ text: text
218
+ }
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Create content_block_stop event
224
+ */
225
+ export function createContentBlockStopEvent(index = 0) {
226
+ return createAnthropicStreamEvent('content_block_stop', {
227
+ type: 'content_block_stop',
228
+ index: index
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Create message_delta event
234
+ */
235
+ export function createMessageDeltaEvent(outputTokens = 0) {
236
+ return createAnthropicStreamEvent('message_delta', {
237
+ type: 'message_delta',
238
+ delta: {
239
+ stop_reason: 'end_turn',
240
+ stop_sequence: null
241
+ },
242
+ usage: {
243
+ output_tokens: outputTokens
244
+ }
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Create message_stop event
250
+ */
251
+ export function createMessageStopEvent() {
252
+ return createAnthropicStreamEvent('message_stop', {
253
+ type: 'message_stop'
254
+ });
255
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Conversation Store Module
3
- * Supports SQLite (persistent) and Memory (in-memory) storage
3
+ * Supports SQLite (persistent via sql.js) and Memory (in-memory) storage
4
4
  */
5
5
 
6
- import Database from 'better-sqlite3';
7
6
  import { fileURLToPath } from 'url';
8
7
  import { dirname, join } from 'path';
8
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
9
9
  import crypto from 'crypto';
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url);
@@ -21,26 +21,66 @@ const MAX_MESSAGES_PER_CONVERSATION = 100;
21
21
  // Memory store (fallback)
22
22
  const memoryStore = new Map();
23
23
 
24
- // SQLite database
24
+ // SQLite database (sql.js)
25
25
  let db = null;
26
+ let SQL = null;
27
+ let dbInitialized = false;
28
+
29
+ // Save database to file periodically
30
+ let saveTimeout = null;
31
+ function scheduleSave() {
32
+ if (saveTimeout) clearTimeout(saveTimeout);
33
+ saveTimeout = setTimeout(() => {
34
+ if (db && STORE_TYPE === 'sqlite') {
35
+ try {
36
+ const data = db.export();
37
+ writeFileSync(DB_PATH, Buffer.from(data));
38
+ } catch (error) {
39
+ console.error('āš ļø Failed to save database:', error.message);
40
+ }
41
+ }
42
+ }, 1000); // Debounce saves by 1 second
43
+ }
44
+
45
+ async function initDatabase() {
46
+ if (dbInitialized) return;
47
+ dbInitialized = true;
26
48
 
27
- function initDatabase() {
28
49
  if (STORE_TYPE === 'memory') {
29
50
  console.log('šŸ“¦ Using in-memory conversation store');
30
51
  return;
31
52
  }
32
53
 
33
54
  try {
34
- db = new Database(DB_PATH);
55
+ // Dynamic import sql.js
56
+ const initSqlJs = (await import('sql.js')).default;
57
+ SQL = await initSqlJs();
35
58
 
36
- db.exec(`
59
+ // Load existing database or create new one
60
+ if (existsSync(DB_PATH)) {
61
+ try {
62
+ const fileBuffer = readFileSync(DB_PATH);
63
+ db = new SQL.Database(fileBuffer);
64
+ console.log(`šŸ“¦ Loaded SQLite database: ${DB_PATH}`);
65
+ } catch (error) {
66
+ console.error('āš ļø Failed to load existing database, creating new one:', error.message);
67
+ db = new SQL.Database();
68
+ }
69
+ } else {
70
+ db = new SQL.Database();
71
+ }
72
+
73
+ // Create tables
74
+ db.run(`
37
75
  CREATE TABLE IF NOT EXISTS conversations (
38
76
  id TEXT PRIMARY KEY,
39
77
  metadata TEXT DEFAULT '{}',
40
78
  created_at INTEGER NOT NULL,
41
79
  updated_at INTEGER NOT NULL
42
- );
43
-
80
+ )
81
+ `);
82
+
83
+ db.run(`
44
84
  CREATE TABLE IF NOT EXISTS messages (
45
85
  id INTEGER PRIMARY KEY AUTOINCREMENT,
46
86
  conversation_id TEXT NOT NULL,
@@ -48,12 +88,15 @@ function initDatabase() {
48
88
  content TEXT NOT NULL,
49
89
  timestamp INTEGER NOT NULL,
50
90
  FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
51
- );
52
-
53
- CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
54
- CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at);
91
+ )
55
92
  `);
56
93
 
94
+ db.run(`CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id)`);
95
+ db.run(`CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at)`);
96
+
97
+ // Save initial database
98
+ scheduleSave();
99
+
57
100
  console.log(`šŸ“¦ Using SQLite conversation store: ${DB_PATH}`);
58
101
  } catch (error) {
59
102
  console.error('āš ļø Failed to initialize SQLite, falling back to memory store:', error.message);
@@ -61,8 +104,8 @@ function initDatabase() {
61
104
  }
62
105
  }
63
106
 
64
- // Initialize database
65
- initDatabase();
107
+ // Export init function for async initialization
108
+ export { initDatabase };
66
109
 
67
110
  export function generateConversationId() {
68
111
  return crypto.randomUUID();
@@ -78,17 +121,27 @@ export function getConversation(conversationId) {
78
121
  return conv;
79
122
  }
80
123
 
81
- const conv = db.prepare('SELECT * FROM conversations WHERE id = ?').get(conversationId);
82
- if (!conv) return null;
83
-
84
- const messages = db.prepare('SELECT role, content, timestamp FROM messages WHERE conversation_id = ? ORDER BY id').all(conversationId);
85
- db.prepare('UPDATE conversations SET updated_at = ? WHERE id = ?').run(Date.now(), conversationId);
124
+ const convResult = db.exec('SELECT * FROM conversations WHERE id = ?', [conversationId]);
125
+ if (!convResult.length || !convResult[0].values.length) return null;
126
+
127
+ const conv = convResult[0].values[0];
128
+ const [id, metadata, created_at, updated_at] = conv;
129
+
130
+ const messagesResult = db.exec('SELECT role, content, timestamp FROM messages WHERE conversation_id = ? ORDER BY id', [conversationId]);
131
+ const messages = messagesResult.length ? messagesResult[0].values.map(row => ({
132
+ role: row[0],
133
+ content: row[1],
134
+ timestamp: row[2]
135
+ })) : [];
136
+
137
+ db.run('UPDATE conversations SET updated_at = ? WHERE id = ?', [Date.now(), conversationId]);
138
+ scheduleSave();
86
139
 
87
140
  return {
88
- id: conv.id,
141
+ id,
89
142
  messages,
90
- metadata: JSON.parse(conv.metadata || '{}'),
91
- createdAt: conv.created_at,
143
+ metadata: JSON.parse(metadata || '{}'),
144
+ createdAt: created_at,
92
145
  updatedAt: Date.now()
93
146
  };
94
147
  }
@@ -103,8 +156,9 @@ export function createConversation(conversationId = null, metadata = {}) {
103
156
  return conv;
104
157
  }
105
158
 
106
- db.prepare('INSERT OR REPLACE INTO conversations (id, metadata, created_at, updated_at) VALUES (?, ?, ?, ?)')
107
- .run(id, JSON.stringify(metadata), now, now);
159
+ db.run('INSERT OR REPLACE INTO conversations (id, metadata, created_at, updated_at) VALUES (?, ?, ?, ?)',
160
+ [id, JSON.stringify(metadata), now, now]);
161
+ scheduleSave();
108
162
 
109
163
  return { id, messages: [], createdAt: now, updatedAt: now, metadata };
110
164
  }
@@ -128,33 +182,49 @@ export function addMessageToConversation(conversationId, role, content) {
128
182
  return conv;
129
183
  }
130
184
 
131
- const existing = db.prepare('SELECT id FROM conversations WHERE id = ?').get(conversationId);
132
- if (!existing) {
185
+ const existing = db.exec('SELECT id FROM conversations WHERE id = ?', [conversationId]);
186
+ if (!existing.length || !existing[0].values.length) {
133
187
  createConversation(conversationId);
134
188
  }
135
189
 
136
- const count = db.prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?').get(conversationId).count;
190
+ const countResult = db.exec('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?', [conversationId]);
191
+ const count = countResult.length ? countResult[0].values[0][0] : 0;
192
+
137
193
  if (count >= MAX_MESSAGES_PER_CONVERSATION) {
138
- const systemMsg = db.prepare("SELECT id FROM messages WHERE conversation_id = ? AND role = 'system' LIMIT 1").get(conversationId);
194
+ const systemMsgResult = db.exec("SELECT id FROM messages WHERE conversation_id = ? AND role = 'system' LIMIT 1", [conversationId]);
139
195
  const deleteCount = count - MAX_MESSAGES_PER_CONVERSATION + 2;
140
196
 
141
- if (systemMsg) {
142
- db.prepare('DELETE FROM messages WHERE conversation_id = ? AND id != ? ORDER BY id LIMIT ?')
143
- .run(conversationId, systemMsg.id, deleteCount);
197
+ if (systemMsgResult.length && systemMsgResult[0].values.length) {
198
+ const systemId = systemMsgResult[0].values[0][0];
199
+ // Delete oldest messages except system message
200
+ const toDeleteResult = db.exec('SELECT id FROM messages WHERE conversation_id = ? AND id != ? ORDER BY id LIMIT ?',
201
+ [conversationId, systemId, deleteCount]);
202
+ if (toDeleteResult.length) {
203
+ toDeleteResult[0].values.forEach(row => {
204
+ db.run('DELETE FROM messages WHERE id = ?', [row[0]]);
205
+ });
206
+ }
144
207
  } else {
145
- db.prepare('DELETE FROM messages WHERE conversation_id = ? ORDER BY id LIMIT ?')
146
- .run(conversationId, deleteCount);
208
+ const toDeleteResult = db.exec('SELECT id FROM messages WHERE conversation_id = ? ORDER BY id LIMIT ?',
209
+ [conversationId, deleteCount]);
210
+ if (toDeleteResult.length) {
211
+ toDeleteResult[0].values.forEach(row => {
212
+ db.run('DELETE FROM messages WHERE id = ?', [row[0]]);
213
+ });
214
+ }
147
215
  }
148
216
  }
149
217
 
150
- db.prepare('INSERT INTO messages (conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
151
- .run(conversationId, role, content, now);
218
+ db.run('INSERT INTO messages (conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?)',
219
+ [conversationId, role, content, now]);
152
220
 
153
- db.prepare('UPDATE conversations SET updated_at = ? WHERE id = ?').run(now, conversationId);
221
+ db.run('UPDATE conversations SET updated_at = ? WHERE id = ?', [now, conversationId]);
222
+ scheduleSave();
154
223
 
155
224
  return getConversation(conversationId);
156
225
  }
157
226
 
227
+
158
228
  export function listAllConversations() {
159
229
  if (STORE_TYPE === 'memory' || !db) {
160
230
  const conversations = [];
@@ -171,17 +241,21 @@ export function listAllConversations() {
171
241
  return conversations.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
172
242
  }
173
243
 
174
- const convs = db.prepare('SELECT * FROM conversations ORDER BY updated_at DESC').all();
175
- return convs.map(conv => {
176
- const lastMsg = db.prepare('SELECT content FROM messages WHERE conversation_id = ? ORDER BY id DESC LIMIT 1').get(conv.id);
177
- const msgCount = db.prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?').get(conv.id).count;
244
+ const convsResult = db.exec('SELECT * FROM conversations ORDER BY updated_at DESC');
245
+ if (!convsResult.length) return [];
246
+
247
+ return convsResult[0].values.map(conv => {
248
+ const [id, metadata, created_at, updated_at] = conv;
249
+ const lastMsgResult = db.exec('SELECT content FROM messages WHERE conversation_id = ? ORDER BY id DESC LIMIT 1', [id]);
250
+ const msgCountResult = db.exec('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?', [id]);
251
+
178
252
  return {
179
- id: conv.id,
180
- message_count: msgCount,
181
- created_at: new Date(conv.created_at).toISOString(),
182
- updated_at: new Date(conv.updated_at).toISOString(),
183
- metadata: JSON.parse(conv.metadata || '{}'),
184
- preview: lastMsg?.content?.substring(0, 100) || ''
253
+ id,
254
+ message_count: msgCountResult.length ? msgCountResult[0].values[0][0] : 0,
255
+ created_at: new Date(created_at).toISOString(),
256
+ updated_at: new Date(updated_at).toISOString(),
257
+ metadata: JSON.parse(metadata || '{}'),
258
+ preview: lastMsgResult.length && lastMsgResult[0].values.length ? lastMsgResult[0].values[0][0]?.substring(0, 100) || '' : ''
185
259
  };
186
260
  });
187
261
  }
@@ -191,9 +265,10 @@ export function deleteConversation(conversationId) {
191
265
  return memoryStore.delete(conversationId);
192
266
  }
193
267
 
194
- db.prepare('DELETE FROM messages WHERE conversation_id = ?').run(conversationId);
195
- const result = db.prepare('DELETE FROM conversations WHERE id = ?').run(conversationId);
196
- return result.changes > 0;
268
+ db.run('DELETE FROM messages WHERE conversation_id = ?', [conversationId]);
269
+ db.run('DELETE FROM conversations WHERE id = ?', [conversationId]);
270
+ scheduleSave();
271
+ return true;
197
272
  }
198
273
 
199
274
  export function clearAllConversations() {
@@ -203,9 +278,11 @@ export function clearAllConversations() {
203
278
  return count;
204
279
  }
205
280
 
206
- const count = db.prepare('SELECT COUNT(*) as count FROM conversations').get().count;
207
- db.prepare('DELETE FROM messages').run();
208
- db.prepare('DELETE FROM conversations').run();
281
+ const countResult = db.exec('SELECT COUNT(*) as count FROM conversations');
282
+ const count = countResult.length ? countResult[0].values[0][0] : 0;
283
+ db.run('DELETE FROM messages');
284
+ db.run('DELETE FROM conversations');
285
+ scheduleSave();
209
286
  return count;
210
287
  }
211
288
 
@@ -213,7 +290,8 @@ export function getConversationCount() {
213
290
  if (STORE_TYPE === 'memory' || !db) {
214
291
  return memoryStore.size;
215
292
  }
216
- return db.prepare('SELECT COUNT(*) as count FROM conversations').get().count;
293
+ const result = db.exec('SELECT COUNT(*) as count FROM conversations');
294
+ return result.length ? result[0].values[0][0] : 0;
217
295
  }
218
296
 
219
297
  export function cleanupConversations() {
@@ -238,23 +316,32 @@ export function cleanupConversations() {
238
316
  return deleted;
239
317
  }
240
318
 
241
- const expiredConvs = db.prepare('SELECT id FROM conversations WHERE updated_at < ?').all(expireTime);
242
- for (const conv of expiredConvs) {
243
- db.prepare('DELETE FROM messages WHERE conversation_id = ?').run(conv.id);
319
+ // Delete expired conversations
320
+ const expiredResult = db.exec('SELECT id FROM conversations WHERE updated_at < ?', [expireTime]);
321
+ let deleted = 0;
322
+ if (expiredResult.length) {
323
+ expiredResult[0].values.forEach(row => {
324
+ db.run('DELETE FROM messages WHERE conversation_id = ?', [row[0]]);
325
+ db.run('DELETE FROM conversations WHERE id = ?', [row[0]]);
326
+ deleted++;
327
+ });
244
328
  }
245
- let result = db.prepare('DELETE FROM conversations WHERE updated_at < ?').run(expireTime);
246
- let deleted = result.changes;
247
329
 
248
- const count = db.prepare('SELECT COUNT(*) as count FROM conversations').get().count;
330
+ // Enforce max conversations limit
331
+ const countResult = db.exec('SELECT COUNT(*) as count FROM conversations');
332
+ const count = countResult.length ? countResult[0].values[0][0] : 0;
249
333
  if (count > MAX_CONVERSATIONS) {
250
- const toDelete = db.prepare('SELECT id FROM conversations ORDER BY updated_at ASC LIMIT ?').all(count - MAX_CONVERSATIONS);
251
- for (const conv of toDelete) {
252
- db.prepare('DELETE FROM messages WHERE conversation_id = ?').run(conv.id);
253
- db.prepare('DELETE FROM conversations WHERE id = ?').run(conv.id);
254
- deleted++;
334
+ const toDeleteResult = db.exec('SELECT id FROM conversations ORDER BY updated_at ASC LIMIT ?', [count - MAX_CONVERSATIONS]);
335
+ if (toDeleteResult.length) {
336
+ toDeleteResult[0].values.forEach(row => {
337
+ db.run('DELETE FROM messages WHERE conversation_id = ?', [row[0]]);
338
+ db.run('DELETE FROM conversations WHERE id = ?', [row[0]]);
339
+ deleted++;
340
+ });
255
341
  }
256
342
  }
257
343
 
344
+ if (deleted > 0) scheduleSave();
258
345
  return deleted;
259
346
  }
260
347
 
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Request Logger Module
3
+ * Pretty prints request/response info using cli-table3
4
+ */
5
+
6
+ import { getConversation } from './conversation-store.js';
7
+
8
+ let Table;
9
+ let tableAvailable = false;
10
+
11
+ // Try to load cli-table3 (optional dependency)
12
+ try {
13
+ Table = (await import('cli-table3')).default;
14
+ tableAvailable = true;
15
+ } catch (e) {
16
+ // cli-table3 not available, use simple logging
17
+ }
18
+
19
+ /**
20
+ * Log stream complete with full info and input stack table
21
+ */
22
+ export function logStreamComplete(info) {
23
+ const {
24
+ conversationId,
25
+ contentLength = 0,
26
+ messageCount = 0,
27
+ agent = 'unknown',
28
+ stream = true
29
+ } = info;
30
+
31
+ const streamMode = stream ? 'stream' : 'sync';
32
+
33
+ // Main info line
34
+ console.log(`šŸ“„ Complete: Conv. ID ${conversationId}, ${contentLength} chars, Messages: ${messageCount}`);
35
+ console.log(` ${agent}(${streamMode})`);
36
+
37
+ // Get history from conversation store
38
+ const conv = getConversation(conversationId);
39
+ const history = conv?.messages || [];
40
+
41
+ if (history.length > 0 && tableAvailable) {
42
+ const table = new Table({
43
+ head: ['#', 'Role', 'Content'],
44
+ colWidths: [4, 12, 60],
45
+ style: { head: ['cyan'] },
46
+ wordWrap: true
47
+ });
48
+
49
+ history.forEach((item, index) => {
50
+ const preview = item.content.length > 80 ? item.content.substring(0, 80) + '...' : item.content;
51
+ table.push([index + 1, item.role, preview]);
52
+ });
53
+
54
+ console.log(' Input Stack:');
55
+ console.log(table.toString());
56
+ } else if (history.length > 0) {
57
+ console.log(' Input Stack:');
58
+ history.forEach((item, index) => {
59
+ const preview = item.content.length > 50 ? item.content.substring(0, 50) + '...' : item.content;
60
+ console.log(` ${index + 1}. [${item.role}] ${preview}`);
61
+ });
62
+ }
63
+
64
+ console.log(''); // Empty line for separation
65
+ }
66
+
67
+ /**
68
+ * Log request (simplified)
69
+ */
70
+ export function logRequest(info) {
71
+ const { conversationId } = info;
72
+ console.log(`šŸ“¤ Request: Conv. ID ${conversationId || 'new'}`);
73
+ }
74
+
75
+ /**
76
+ * Log response (for non-streaming)
77
+ */
78
+ export function logResponse(info) {
79
+ logStreamComplete(info);
80
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Anthropic Messages Route
3
+ * Handles /v1/messages endpoint (Anthropic/Claude API compatible)
4
+ */
5
+
6
+ import { Router } from 'express';
7
+ import { callYouApi } from '../api-client.js';
8
+ import {
9
+ mapAnthropicToYouParams,
10
+ convertToAnthropicResponse,
11
+ createMessageStartEvent,
12
+ createContentBlockStartEvent,
13
+ createContentBlockDeltaEvent,
14
+ createContentBlockStopEvent,
15
+ createMessageDeltaEvent,
16
+ createMessageStopEvent
17
+ } from '../anthropic-mapper.js';
18
+ import {
19
+ getConversation,
20
+ createConversation,
21
+ addMessageToConversation,
22
+ generateConversationId
23
+ } from '../conversation-store.js';
24
+ import { logRequest, logStreamComplete, logResponse } from '../request-logger.js';
25
+
26
+ const router = Router();
27
+
28
+ // Get API key with rotation support
29
+ function getApiKey() {
30
+ const keys = (process.env.YDC_API_KEYS || process.env.YDC_API_KEY || '').split(',').filter(k => k.trim());
31
+ if (keys.length === 0) throw new Error('No API key configured');
32
+ return keys[Math.floor(Math.random() * keys.length)].trim();
33
+ }
34
+
35
+ // Anthropic Messages endpoint
36
+ router.post('/v1/messages', async (req, res) => {
37
+ try {
38
+ const {
39
+ model = 'claude-3-5-sonnet-20241022',
40
+ messages,
41
+ system,
42
+ max_tokens = 1024,
43
+ temperature = 0.7,
44
+ stream = false,
45
+ metadata
46
+ } = req.body;
47
+
48
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
49
+ return res.status(400).json({
50
+ type: 'error',
51
+ error: {
52
+ type: 'invalid_request_error',
53
+ message: 'messages is required and must be a non-empty array'
54
+ }
55
+ });
56
+ }
57
+
58
+ const apiKey = getApiKey();
59
+
60
+ // Handle conversation persistence via metadata or generate new one
61
+ let conversationId = metadata?.conversation_id || generateConversationId();
62
+ const existingConv = getConversation(conversationId);
63
+ if (!existingConv) {
64
+ createConversation(conversationId);
65
+ }
66
+
67
+ // Map to You.com parameters
68
+ const youParams = mapAnthropicToYouParams({
69
+ model,
70
+ messages,
71
+ system,
72
+ temperature,
73
+ max_tokens,
74
+ stream
75
+ });
76
+
77
+ // Get current user input for logging
78
+ const lastUserMsg = messages.filter(m => m.role === 'user').pop();
79
+ const currentInput = typeof lastUserMsg?.content === 'string'
80
+ ? lastUserMsg.content
81
+ : lastUserMsg?.content?.filter(c => c.type === 'text').map(c => c.text).join('\n') || '';
82
+
83
+ logRequest({
84
+ endpoint: '/v1/messages (Anthropic)',
85
+ agent: youParams.agent,
86
+ model,
87
+ stream,
88
+ conversationId,
89
+ messageCount: messages.length,
90
+ input: currentInput
91
+ });
92
+
93
+ // Store user message
94
+ if (conversationId) {
95
+ const lastUserMsg = messages.filter(m => m.role === 'user').pop();
96
+ if (lastUserMsg) {
97
+ const content = typeof lastUserMsg.content === 'string'
98
+ ? lastUserMsg.content
99
+ : lastUserMsg.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
100
+ addMessageToConversation(conversationId, 'user', content);
101
+ }
102
+ }
103
+
104
+ if (stream) {
105
+ // Streaming response
106
+ res.setHeader('Content-Type', 'text/event-stream');
107
+ res.setHeader('Cache-Control', 'no-cache');
108
+ res.setHeader('Connection', 'keep-alive');
109
+
110
+ // Send message_start with conversation_id
111
+ res.write(createMessageStartEvent(model, conversationId));
112
+
113
+ // Send content_block_start
114
+ res.write(createContentBlockStartEvent(0));
115
+
116
+ try {
117
+ const response = await callYouApi(apiKey, { ...youParams, stream: true });
118
+
119
+ let fullContent = '';
120
+ let buffer = '';
121
+
122
+ // Use Web Streams API (ReadableStream)
123
+ const reader = response.body.getReader();
124
+ const decoder = new TextDecoder();
125
+
126
+ const processStream = async () => {
127
+ try {
128
+ while (true) {
129
+ const { done, value } = await reader.read();
130
+ if (done) break;
131
+
132
+ buffer += decoder.decode(value, { stream: true });
133
+ const lines = buffer.split('\n');
134
+ buffer = lines.pop() || '';
135
+
136
+ for (const line of lines) {
137
+ if (line.startsWith('data: ')) {
138
+ const data = line.slice(6);
139
+ if (data === '[DONE]') continue;
140
+
141
+ try {
142
+ const parsed = JSON.parse(data);
143
+
144
+ // Handle streaming delta format
145
+ if (parsed.type === 'response.output_text.delta' &&
146
+ parsed.response?.type === 'message.answer' &&
147
+ parsed.response?.delta) {
148
+ const newText = parsed.response.delta;
149
+ if (newText) {
150
+ fullContent += newText;
151
+ res.write(createContentBlockDeltaEvent(newText, 0));
152
+ }
153
+ }
154
+
155
+ // Also handle full output format (non-streaming fallback)
156
+ if (parsed.output) {
157
+ for (const item of parsed.output) {
158
+ if (item.type === 'message.answer' && item.text) {
159
+ const newText = item.text.slice(fullContent.length);
160
+ if (newText) {
161
+ fullContent = item.text;
162
+ res.write(createContentBlockDeltaEvent(newText, 0));
163
+ }
164
+ }
165
+ }
166
+ }
167
+ } catch (e) {
168
+ // Skip invalid JSON
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // Store assistant response
175
+ if (conversationId && fullContent) {
176
+ addMessageToConversation(conversationId, 'assistant', fullContent);
177
+ }
178
+
179
+ logStreamComplete({
180
+ conversationId,
181
+ contentLength: fullContent.length,
182
+ messageCount: messages.length + 1,
183
+ agent: youParams.agent,
184
+ stream: true,
185
+ responsePreview: fullContent
186
+ });
187
+
188
+ // Send closing events
189
+ res.write(createContentBlockStopEvent(0));
190
+ res.write(createMessageDeltaEvent(Math.floor(fullContent.length / 4)));
191
+ res.write(createMessageStopEvent());
192
+ res.end();
193
+ } catch (error) {
194
+ console.error('Stream processing error:', error);
195
+ res.write(createContentBlockDeltaEvent(`Error: ${error.message}`, 0));
196
+ res.write(createContentBlockStopEvent(0));
197
+ res.write(createMessageDeltaEvent(0));
198
+ res.write(createMessageStopEvent());
199
+ res.end();
200
+ }
201
+ };
202
+
203
+ processStream();
204
+
205
+ } catch (error) {
206
+ console.error('Streaming error:', error);
207
+ res.write(createContentBlockDeltaEvent(`Error: ${error.message}`, 0));
208
+ res.write(createContentBlockStopEvent(0));
209
+ res.write(createMessageDeltaEvent(0));
210
+ res.write(createMessageStopEvent());
211
+ res.end();
212
+ }
213
+
214
+ } else {
215
+ // Non-streaming response
216
+ const response = await callYouApi(apiKey, youParams);
217
+ const data = await response.json();
218
+
219
+ console.log('šŸ“„ You.com response received');
220
+
221
+ const anthropicResponse = convertToAnthropicResponse(data, model);
222
+
223
+ // Store assistant response
224
+ if (conversationId) {
225
+ const content = anthropicResponse.content[0]?.text || '';
226
+ if (content) {
227
+ addMessageToConversation(conversationId, 'assistant', content);
228
+ }
229
+ // Add conversation_id to response
230
+ anthropicResponse.metadata = { conversation_id: conversationId };
231
+ }
232
+
233
+ res.json(anthropicResponse);
234
+ }
235
+
236
+ } catch (error) {
237
+ console.error('Anthropic messages error:', error);
238
+ res.status(500).json({
239
+ type: 'error',
240
+ error: {
241
+ type: 'api_error',
242
+ message: error.message
243
+ }
244
+ });
245
+ }
246
+ });
247
+
248
+ export default router;
@@ -5,13 +5,13 @@
5
5
  import { Router } from 'express';
6
6
  import { authenticate } from '../auth-middleware.js';
7
7
  import { mapOpenAIToYouParams, convertToOpenAIResponse, createStreamChunk } from '../openai-mapper.js';
8
- import { callYouApi, extractText } from '../api-client.js';
8
+ import { callYouApi } from '../api-client.js';
9
9
  import {
10
10
  getConversation,
11
- createConversation,
12
11
  addMessageToConversation,
13
12
  generateConversationId
14
13
  } from '../conversation-store.js';
14
+ import { logRequest, logStreamComplete } from '../request-logger.js';
15
15
 
16
16
  const router = Router();
17
17
  const API_KEY = process.env.YDC_API_KEY;
@@ -69,8 +69,19 @@ router.post('/v1/chat/completions', authenticate, async (req, res) => {
69
69
 
70
70
  const youParams = mapOpenAIToYouParams({ ...req.body, messages: fullMessages });
71
71
 
72
- console.log('šŸ“¤ Sending request to You.com:', JSON.stringify({ ...youParams, input: youParams.input.substring(0, 200) + '...' }, null, 2));
73
- console.log(`šŸ’¬ Conversation ID: ${conversationId}, Messages: ${fullMessages.length}`);
72
+ // Get current user input for logging
73
+ const currentUserMsg = fullMessages.filter(m => m.role === 'user').pop();
74
+ const currentInput = currentUserMsg?.content || '';
75
+
76
+ logRequest({
77
+ endpoint: '/v1/chat/completions (OpenAI)',
78
+ agent: youParams.agent,
79
+ model: req.body.model,
80
+ stream: req.body.stream || false,
81
+ conversationId,
82
+ messageCount: fullMessages.length,
83
+ input: currentInput
84
+ });
74
85
 
75
86
  const timeoutMs = youParams.timeout || (youParams.agent === 'advanced' ? 3000000 : 300000);
76
87
 
@@ -120,7 +131,9 @@ async function handleStreamingResponse(req, res, response, youParams, conversati
120
131
  const reader = response.body.getReader();
121
132
  const decoder = new TextDecoder();
122
133
  let buffer = '';
134
+ let fullContent = '';
123
135
  const model = req.body.model || 'advanced';
136
+ const messageCount = req.body.messages?.length || 0;
124
137
  const STREAM_TIMEOUT = youParams.timeout || (youParams.agent === 'advanced' ? 3000000 : 300000);
125
138
 
126
139
  try {
@@ -159,6 +172,7 @@ async function handleStreamingResponse(req, res, response, youParams, conversati
159
172
  res.end();
160
173
  }, STREAM_TIMEOUT);
161
174
 
175
+ fullContent += data.response.delta;
162
176
  const chunk = createStreamChunk(model, data.response.delta);
163
177
  res.write(`data: ${JSON.stringify(chunk)}\n\n`);
164
178
  }
@@ -170,6 +184,22 @@ async function handleStreamingResponse(req, res, response, youParams, conversati
170
184
  }
171
185
 
172
186
  clearTimeout(streamTimeout);
187
+
188
+ // Store assistant response
189
+ if (conversationId && fullContent) {
190
+ addMessageToConversation(conversationId, 'assistant', fullContent);
191
+ }
192
+
193
+ // Log completion
194
+ logStreamComplete({
195
+ conversationId,
196
+ contentLength: fullContent.length,
197
+ messageCount: messageCount + 1,
198
+ agent: youParams.agent,
199
+ stream: true,
200
+ responsePreview: fullContent
201
+ });
202
+
173
203
  } catch (streamError) {
174
204
  console.error('āŒ Streaming error:', streamError);
175
205
  res.write(`data: {"error": "Streaming error: ${streamError.message}"}\n\n`);
package/openai-server.js CHANGED
@@ -15,9 +15,10 @@ import chatRoutes from './lib/routes/chat.js';
15
15
  import modelsRoutes from './lib/routes/models.js';
16
16
  import conversationsRoutes from './lib/routes/conversations.js';
17
17
  import healthRoutes from './lib/routes/health.js';
18
+ import anthropicRoutes from './lib/routes/anthropic-messages.js';
18
19
 
19
20
  // Import config
20
- import { storeConfig } from './lib/conversation-store.js';
21
+ import { storeConfig, initDatabase } from './lib/conversation-store.js';
21
22
  import { authConfig } from './lib/auth-middleware.js';
22
23
  import { listAdvancedVersions, getDefaultAdvancedVersion } from './lib/advanced-versions.js';
23
24
 
@@ -56,10 +57,14 @@ app.use(chatRoutes);
56
57
  app.use(modelsRoutes);
57
58
  app.use(conversationsRoutes);
58
59
  app.use(healthRoutes);
60
+ app.use(anthropicRoutes);
59
61
 
60
62
  // Start server with auto port detection
61
63
  async function startServer() {
62
64
  try {
65
+ // Initialize database (handles missing better-sqlite3 gracefully)
66
+ await initDatabase();
67
+
63
68
  const port = await findAvailablePort(startPort);
64
69
  app.set('port', port);
65
70
 
@@ -70,7 +75,8 @@ async function startServer() {
70
75
  console.log(`šŸ“¦ Conversation Store: ${storeConfig.STORE_TYPE}${storeConfig.STORE_TYPE === 'sqlite' && storeConfig.isDbConnected() ? ` (${storeConfig.DB_PATH})` : ''}`);
71
76
  console.log(`šŸ” Token Auth: ${authConfig.REQUIRE_TOKEN_AUTH ? `enabled (${authConfig.ACCESS_TOKENS_COUNT} tokens)` : 'disabled (accept all)'}`);
72
77
  console.log(`\nšŸ“– Endpoints:`);
73
- console.log(` POST http://localhost:${port}/v1/chat/completions`);
78
+ console.log(` POST http://localhost:${port}/v1/chat/completions (OpenAI)`);
79
+ console.log(` POST http://localhost:${port}/v1/messages (Anthropic/Claude)`);
74
80
  console.log(` GET http://localhost:${port}/v1/models`);
75
81
  console.log(` GET http://localhost:${port}/v1/versions`);
76
82
  console.log(` GET http://localhost:${port}/health`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ydc-mcp-server",
3
- "version": "1.6.1",
4
- "description": "MCP server for You.com Agents API - Express, Research, Advanced agents with multi-turn conversations, streaming, and OpenAI compatibility",
3
+ "version": "1.7.8",
4
+ "description": "MCP server for You.com Agents API - Express, Research, Advanced agents with multi-turn conversations, streaming, OpenAI and Anthropic/Claude API compatibility",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
@@ -27,10 +27,10 @@
27
27
  "prepublishOnly": "echo 'Ready to publish'"
28
28
  },
29
29
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.25.1"
30
+ "@modelcontextprotocol/sdk": "^1.25.1",
31
+ "sql.js": "^1.11.0"
31
32
  },
32
33
  "optionalDependencies": {
33
- "better-sqlite3": "^11.7.0",
34
34
  "chalk": "^5.6.2",
35
35
  "cli-progress": "^3.12.0",
36
36
  "cli-table3": "^0.6.5",