ydc-mcp-server 1.7.0 → 1.7.10

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.
@@ -165,22 +165,29 @@ export function createAnthropicStreamEvent(eventType, data) {
165
165
  /**
166
166
  * Create message_start event
167
167
  */
168
- export function createMessageStartEvent(model) {
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
+
169
188
  return createAnthropicStreamEvent('message_start', {
170
189
  type: 'message_start',
171
- message: {
172
- id: `msg_${Date.now()}`,
173
- type: 'message',
174
- role: 'assistant',
175
- content: [],
176
- model: model,
177
- stop_reason: null,
178
- stop_sequence: null,
179
- usage: {
180
- input_tokens: 0,
181
- output_tokens: 0
182
- }
183
- }
190
+ message
184
191
  });
185
192
  }
186
193
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Request Logger Module
3
+ * Pretty prints request/response info using cli-table3
4
+ */
5
+
6
+ let Table;
7
+ let tableAvailable = false;
8
+
9
+ // Try to load cli-table3 (optional dependency)
10
+ try {
11
+ Table = (await import('cli-table3')).default;
12
+ tableAvailable = true;
13
+ } catch (e) {
14
+ // cli-table3 not available, use simple logging
15
+ }
16
+
17
+ // Track last request ID for comparison
18
+ let lastRequestId = null;
19
+
20
+ // Helper to create table
21
+ function createTable() {
22
+ return new Table({
23
+ colWidths: [4, 12, 60],
24
+ wordWrap: true,
25
+ chars: {
26
+ 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
27
+ 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
28
+ 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
29
+ 'right': '│', 'right-mid': '┤', 'middle': '│'
30
+ }
31
+ });
32
+ }
33
+
34
+ // Helper to print history
35
+ function printHistory(history, label) {
36
+ if (history.length === 0) return;
37
+
38
+ if (tableAvailable) {
39
+ const table = createTable();
40
+ history.forEach((item, index) => {
41
+ const content = item.content || '';
42
+ const preview = content.length > 80 ? content.substring(0, 80) + '...' : content;
43
+ table.push([index + 1, item.role, preview]);
44
+ });
45
+ console.log(` ${label}:`);
46
+ console.log(table.toString());
47
+ } else {
48
+ console.log(` ${label}:`);
49
+ history.forEach((item, index) => {
50
+ const content = item.content || '';
51
+ const preview = content.length > 50 ? content.substring(0, 50) + '...' : content;
52
+ console.log(` ${index + 1}. [${item.role}] ${preview}`);
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Log request with history
59
+ */
60
+ export function logRequest(info) {
61
+ const {
62
+ conversationId,
63
+ agent = 'unknown',
64
+ stream = false,
65
+ messageCount = 0,
66
+ inputMessages = []
67
+ } = info;
68
+
69
+ const shortId = conversationId ? conversationId.split('-')[0] : 'new';
70
+ lastRequestId = shortId;
71
+ const streamMode = stream ? 'stream' : 'sync';
72
+
73
+ console.log(`📤 Request: ${shortId}, Messages: ${messageCount}`);
74
+ console.log(` ${agent}(${streamMode})`);
75
+
76
+ printHistory(inputMessages, 'History');
77
+ }
78
+
79
+ /**
80
+ * Log stream complete with history
81
+ */
82
+ export function logStreamComplete(info) {
83
+ const {
84
+ conversationId,
85
+ contentLength = 0,
86
+ messageCount = 0,
87
+ agent = 'unknown',
88
+ stream = true,
89
+ inputMessages = []
90
+ } = info;
91
+
92
+ const streamMode = stream ? 'stream' : 'sync';
93
+ const shortId = conversationId ? conversationId.split('-')[0] : 'new';
94
+
95
+ // 如果 Complete ID 和 Request ID 不同,顯示括號
96
+ let idDisplay;
97
+ if (lastRequestId && lastRequestId !== shortId) {
98
+ idDisplay = `(${shortId})`;
99
+ } else {
100
+ idDisplay = shortId;
101
+ }
102
+
103
+ console.log(`📥 Complete: ${idDisplay}, ${contentLength} chars, Messages: ${messageCount}`);
104
+ console.log(` ${agent}(${streamMode})`);
105
+
106
+ printHistory(inputMessages, 'History');
107
+ console.log('');
108
+ }
109
+
110
+ /**
111
+ * Log response (for non-streaming)
112
+ */
113
+ export function logResponse(info) {
114
+ logStreamComplete(info);
115
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Router } from 'express';
7
- import { callYouApi, extractText } from '../api-client.js';
7
+ import { callYouApi } from '../api-client.js';
8
8
  import {
9
9
  mapAnthropicToYouParams,
10
10
  convertToAnthropicResponse,
@@ -19,8 +19,9 @@ import {
19
19
  getConversation,
20
20
  createConversation,
21
21
  addMessageToConversation,
22
- generateConversationId
22
+ generateConversationId
23
23
  } from '../conversation-store.js';
24
+ import { logRequest, logStreamComplete, logResponse } from '../request-logger.js';
24
25
 
25
26
  const router = Router();
26
27
 
@@ -56,13 +57,11 @@ router.post('/v1/messages', async (req, res) => {
56
57
 
57
58
  const apiKey = getApiKey();
58
59
 
59
- // Handle conversation persistence via metadata
60
- let conversationId = metadata?.conversation_id;
61
- if (conversationId) {
62
- const existingConv = getConversation(conversationId);
63
- if (!existingConv) {
64
- createConversation(conversationId);
65
- }
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);
66
65
  }
67
66
 
68
67
  // Map to You.com parameters
@@ -75,6 +74,35 @@ router.post('/v1/messages', async (req, res) => {
75
74
  stream
76
75
  });
77
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
+ // Build full messages for logging (including system prompt)
84
+ const fullMessages = [];
85
+ if (system) {
86
+ fullMessages.push({ role: 'system', content: system });
87
+ }
88
+ messages.forEach(m => {
89
+ const content = typeof m.content === 'string'
90
+ ? m.content
91
+ : m.content?.filter(c => c.type === 'text').map(c => c.text).join('\n') || '';
92
+ fullMessages.push({ role: m.role, content });
93
+ });
94
+
95
+ logRequest({
96
+ endpoint: '/v1/messages (Anthropic)',
97
+ agent: youParams.agent,
98
+ model,
99
+ stream,
100
+ conversationId,
101
+ messageCount: fullMessages.length,
102
+ input: currentInput,
103
+ inputMessages: fullMessages
104
+ });
105
+
78
106
  // Store user message
79
107
  if (conversationId) {
80
108
  const lastUserMsg = messages.filter(m => m.role === 'user').pop();
@@ -92,8 +120,8 @@ router.post('/v1/messages', async (req, res) => {
92
120
  res.setHeader('Cache-Control', 'no-cache');
93
121
  res.setHeader('Connection', 'keep-alive');
94
122
 
95
- // Send message_start
96
- res.write(createMessageStartEvent(model));
123
+ // Send message_start with conversation_id
124
+ res.write(createMessageStartEvent(model, conversationId));
97
125
 
98
126
  // Send content_block_start
99
127
  res.write(createContentBlockStartEvent(0));
@@ -104,53 +132,89 @@ router.post('/v1/messages', async (req, res) => {
104
132
  let fullContent = '';
105
133
  let buffer = '';
106
134
 
107
- response.body.on('data', (chunk) => {
108
- buffer += chunk.toString();
109
- const lines = buffer.split('\n');
110
- buffer = lines.pop() || '';
111
-
112
- for (const line of lines) {
113
- if (line.startsWith('data: ')) {
114
- const data = line.slice(6);
115
- if (data === '[DONE]') continue;
116
-
117
- try {
118
- const parsed = JSON.parse(data);
119
- if (parsed.output) {
120
- for (const item of parsed.output) {
121
- if (item.type === 'message.answer' && item.text) {
122
- const newText = item.text.slice(fullContent.length);
135
+ // Use Web Streams API (ReadableStream)
136
+ const reader = response.body.getReader();
137
+ const decoder = new TextDecoder();
138
+
139
+ const processStream = async () => {
140
+ try {
141
+ while (true) {
142
+ const { done, value } = await reader.read();
143
+ if (done) break;
144
+
145
+ buffer += decoder.decode(value, { stream: true });
146
+ const lines = buffer.split('\n');
147
+ buffer = lines.pop() || '';
148
+
149
+ for (const line of lines) {
150
+ if (line.startsWith('data: ')) {
151
+ const data = line.slice(6);
152
+ if (data === '[DONE]') continue;
153
+
154
+ try {
155
+ const parsed = JSON.parse(data);
156
+
157
+ // Handle streaming delta format
158
+ if (parsed.type === 'response.output_text.delta' &&
159
+ parsed.response?.type === 'message.answer' &&
160
+ parsed.response?.delta) {
161
+ const newText = parsed.response.delta;
123
162
  if (newText) {
124
- fullContent = item.text;
163
+ fullContent += newText;
125
164
  res.write(createContentBlockDeltaEvent(newText, 0));
126
165
  }
127
166
  }
167
+
168
+ // Also handle full output format (non-streaming fallback)
169
+ if (parsed.output) {
170
+ for (const item of parsed.output) {
171
+ if (item.type === 'message.answer' && item.text) {
172
+ const newText = item.text.slice(fullContent.length);
173
+ if (newText) {
174
+ fullContent = item.text;
175
+ res.write(createContentBlockDeltaEvent(newText, 0));
176
+ }
177
+ }
178
+ }
179
+ }
180
+ } catch (e) {
181
+ // Skip invalid JSON
128
182
  }
129
183
  }
130
- } catch (e) {
131
- // Skip invalid JSON
132
184
  }
133
185
  }
134
- }
135
- });
136
186
 
137
- response.body.on('end', () => {
138
- // Store assistant response
139
- if (conversationId && fullContent) {
140
- addMessageToConversation(conversationId, 'assistant', fullContent);
141
- }
187
+ // Store assistant response
188
+ if (conversationId && fullContent) {
189
+ addMessageToConversation(conversationId, 'assistant', fullContent);
190
+ }
142
191
 
143
- // Send closing events
144
- res.write(createContentBlockStopEvent(0));
145
- res.write(createMessageDeltaEvent(Math.floor(fullContent.length / 4)));
146
- res.write(createMessageStopEvent());
147
- res.end();
148
- });
192
+ logStreamComplete({
193
+ conversationId,
194
+ contentLength: fullContent.length,
195
+ messageCount: fullMessages.length + 1,
196
+ agent: youParams.agent,
197
+ stream: true,
198
+ responsePreview: fullContent,
199
+ inputMessages: [...fullMessages, { role: 'assistant', content: fullContent }]
200
+ });
149
201
 
150
- response.body.on('error', (error) => {
151
- console.error('Stream error:', error);
152
- res.end();
153
- });
202
+ // Send closing events
203
+ res.write(createContentBlockStopEvent(0));
204
+ res.write(createMessageDeltaEvent(Math.floor(fullContent.length / 4)));
205
+ res.write(createMessageStopEvent());
206
+ res.end();
207
+ } catch (error) {
208
+ console.error('Stream processing error:', error);
209
+ res.write(createContentBlockDeltaEvent(`Error: ${error.message}`, 0));
210
+ res.write(createContentBlockStopEvent(0));
211
+ res.write(createMessageDeltaEvent(0));
212
+ res.write(createMessageStopEvent());
213
+ res.end();
214
+ }
215
+ };
216
+
217
+ processStream();
154
218
 
155
219
  } catch (error) {
156
220
  console.error('Streaming error:', error);
@@ -166,18 +230,27 @@ router.post('/v1/messages', async (req, res) => {
166
230
  const response = await callYouApi(apiKey, youParams);
167
231
  const data = await response.json();
168
232
 
233
+ console.log('📥 You.com response received');
234
+
169
235
  const anthropicResponse = convertToAnthropicResponse(data, model);
170
236
 
171
237
  // Store assistant response
172
- if (conversationId) {
173
- const content = anthropicResponse.content[0]?.text || '';
174
- if (content) {
175
- addMessageToConversation(conversationId, 'assistant', content);
176
- }
238
+ const assistantContent = anthropicResponse.content[0]?.text || '';
239
+ if (conversationId && assistantContent) {
240
+ addMessageToConversation(conversationId, 'assistant', assistantContent);
177
241
  // Add conversation_id to response
178
242
  anthropicResponse.metadata = { conversation_id: conversationId };
179
243
  }
180
244
 
245
+ logStreamComplete({
246
+ conversationId,
247
+ contentLength: assistantContent.length,
248
+ messageCount: fullMessages.length + 1,
249
+ agent: youParams.agent,
250
+ stream: false,
251
+ inputMessages: [...fullMessages, { role: 'assistant', content: assistantContent }]
252
+ });
253
+
181
254
  res.json(anthropicResponse);
182
255
  }
183
256
 
@@ -5,19 +5,27 @@
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;
18
18
 
19
19
  router.post('/v1/chat/completions', authenticate, async (req, res) => {
20
20
  try {
21
+ // Debug: log raw request
22
+ console.log('📨 Raw request body:', JSON.stringify({
23
+ conversation_id: req.body.conversation_id,
24
+ model: req.body.model,
25
+ messages_count: req.body.messages?.length,
26
+ messages: req.body.messages?.map(m => ({ role: m.role, content: m.content?.substring(0, 50) }))
27
+ }, null, 2));
28
+
21
29
  if (!API_KEY) {
22
30
  return res.status(500).json({
23
31
  error: {
@@ -69,17 +77,29 @@ router.post('/v1/chat/completions', authenticate, async (req, res) => {
69
77
 
70
78
  const youParams = mapOpenAIToYouParams({ ...req.body, messages: fullMessages });
71
79
 
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}`);
80
+ // Get current user input for logging
81
+ const currentUserMsg = fullMessages.filter(m => m.role === 'user').pop();
82
+ const currentInput = currentUserMsg?.content || '';
83
+
84
+ logRequest({
85
+ endpoint: '/v1/chat/completions (OpenAI)',
86
+ agent: youParams.agent,
87
+ model: req.body.model,
88
+ stream: req.body.stream || false,
89
+ conversationId,
90
+ messageCount: fullMessages.length,
91
+ input: currentInput,
92
+ inputMessages: fullMessages
93
+ });
74
94
 
75
95
  const timeoutMs = youParams.timeout || (youParams.agent === 'advanced' ? 3000000 : 300000);
76
96
 
77
97
  if (req.body.stream) {
78
98
  const response = await callYouApi(API_KEY, { ...youParams, stream: true }, { timeout: timeoutMs });
79
- await handleStreamingResponse(req, res, response, youParams, conversationId);
99
+ await handleStreamingResponse(req, res, response, youParams, conversationId, fullMessages);
80
100
  } else {
81
101
  const response = await callYouApi(API_KEY, youParams, { timeout: timeoutMs });
82
- await handleNonStreamingResponse(req, res, response, conversationId);
102
+ await handleNonStreamingResponse(req, res, response, conversationId, fullMessages);
83
103
  }
84
104
 
85
105
  } catch (error) {
@@ -105,7 +125,7 @@ router.post('/v1/chat/completions', authenticate, async (req, res) => {
105
125
  }
106
126
  });
107
127
 
108
- async function handleStreamingResponse(req, res, response, youParams, conversationId) {
128
+ async function handleStreamingResponse(req, res, response, youParams, conversationId, inputMessages = []) {
109
129
  res.setHeader('Content-Type', 'text/event-stream');
110
130
  res.setHeader('Cache-Control', 'no-cache');
111
131
  res.setHeader('Connection', 'keep-alive');
@@ -120,7 +140,9 @@ async function handleStreamingResponse(req, res, response, youParams, conversati
120
140
  const reader = response.body.getReader();
121
141
  const decoder = new TextDecoder();
122
142
  let buffer = '';
143
+ let fullContent = '';
123
144
  const model = req.body.model || 'advanced';
145
+ const messageCount = req.body.messages?.length || 0;
124
146
  const STREAM_TIMEOUT = youParams.timeout || (youParams.agent === 'advanced' ? 3000000 : 300000);
125
147
 
126
148
  try {
@@ -159,6 +181,7 @@ async function handleStreamingResponse(req, res, response, youParams, conversati
159
181
  res.end();
160
182
  }, STREAM_TIMEOUT);
161
183
 
184
+ fullContent += data.response.delta;
162
185
  const chunk = createStreamChunk(model, data.response.delta);
163
186
  res.write(`data: ${JSON.stringify(chunk)}\n\n`);
164
187
  }
@@ -170,6 +193,23 @@ async function handleStreamingResponse(req, res, response, youParams, conversati
170
193
  }
171
194
 
172
195
  clearTimeout(streamTimeout);
196
+
197
+ // Store assistant response
198
+ if (conversationId && fullContent) {
199
+ addMessageToConversation(conversationId, 'assistant', fullContent);
200
+ }
201
+
202
+ // Log completion
203
+ logStreamComplete({
204
+ conversationId,
205
+ contentLength: fullContent.length,
206
+ messageCount: messageCount + 1,
207
+ agent: youParams.agent,
208
+ stream: true,
209
+ responsePreview: fullContent,
210
+ inputMessages: inputMessages
211
+ });
212
+
173
213
  } catch (streamError) {
174
214
  console.error('❌ Streaming error:', streamError);
175
215
  res.write(`data: {"error": "Streaming error: ${streamError.message}"}\n\n`);
@@ -181,7 +221,7 @@ async function handleStreamingResponse(req, res, response, youParams, conversati
181
221
  }
182
222
  }
183
223
 
184
- async function handleNonStreamingResponse(req, res, response, conversationId) {
224
+ async function handleNonStreamingResponse(req, res, response, conversationId, inputMessages = []) {
185
225
  const data = await response.json();
186
226
  console.log('📥 You.com response:', JSON.stringify(data, null, 2));
187
227
 
@@ -192,6 +232,16 @@ async function handleNonStreamingResponse(req, res, response, conversationId) {
192
232
  addMessageToConversation(conversationId, 'assistant', assistantContent);
193
233
  }
194
234
 
235
+ // Log completion
236
+ logStreamComplete({
237
+ conversationId,
238
+ contentLength: assistantContent?.length || 0,
239
+ messageCount: inputMessages.length + 1,
240
+ agent: req.body.model || 'advanced',
241
+ stream: false,
242
+ inputMessages: inputMessages
243
+ });
244
+
195
245
  openaiResponse.conversation_id = conversationId;
196
246
  res.json(openaiResponse);
197
247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydc-mcp-server",
3
- "version": "1.7.0",
3
+ "version": "1.7.10",
4
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",