@youdotcom-oss/mcp 1.3.4 → 1.3.5-next.12
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/README.md +2 -2
- package/bin/stdio.js +168 -191
- package/package.json +18 -41
- package/src/contents/register-contents-tool.ts +90 -0
- package/src/contents/tests/contents.utils.spec.ts +188 -0
- package/src/express/register-express-tool.ts +67 -0
- package/src/express/tests/express.utils.spec.ts +244 -0
- package/src/get-mcp-server.ts +17 -0
- package/src/http.ts +72 -0
- package/src/search/register-search-tool.ts +87 -0
- package/src/search/tests/search.utils.spec.ts +217 -0
- package/src/shared/generate-error-report-link.ts +37 -0
- package/src/shared/get-logger.ts +10 -0
- package/src/shared/tests/shared.utils.spec.ts +160 -0
- package/src/shared/use-client-version.ts +21 -0
- package/src/stdio.ts +24 -0
- package/src/tests/exports.spec.ts +24 -0
- package/src/tests/http.spec.ts +318 -0
- package/src/tests/tool.spec.ts +496 -0
- package/src/utils.ts +8 -0
- package/AGENTS.md +0 -730
- package/CONTRIBUTING.md +0 -246
- package/docs/API.md +0 -319
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from 'bun:test';
|
|
2
|
+
import httpApp from '../http.ts';
|
|
3
|
+
|
|
4
|
+
// Increase default timeout for hooks to prevent intermittent failures
|
|
5
|
+
setDefaultTimeout(15_000);
|
|
6
|
+
|
|
7
|
+
let server: ReturnType<typeof Bun.serve>;
|
|
8
|
+
let baseUrl: string;
|
|
9
|
+
const testApiKey = process.env.YDC_API_KEY;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
// Start HTTP server on random port
|
|
13
|
+
const port = Math.floor(Math.random() * 10000) + 20000;
|
|
14
|
+
baseUrl = `http://localhost:${port}`;
|
|
15
|
+
|
|
16
|
+
// Start actual HTTP server using Bun
|
|
17
|
+
server = Bun.serve({
|
|
18
|
+
port,
|
|
19
|
+
fetch: httpApp.fetch.bind(httpApp),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Wait a bit for server to start
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
if (server) {
|
|
28
|
+
server.stop();
|
|
29
|
+
// Wait a bit for server to fully stop
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('HTTP Server Endpoints', () => {
|
|
35
|
+
test('health endpoint returns service status', async () => {
|
|
36
|
+
const response = await fetch(`${baseUrl}/mcp-health`);
|
|
37
|
+
|
|
38
|
+
expect(response.status).toBe(200);
|
|
39
|
+
expect(response.headers.get('content-type')).toContain('application/json');
|
|
40
|
+
|
|
41
|
+
const data = (await response.json()) as {
|
|
42
|
+
status: string;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
version: string;
|
|
45
|
+
service: string;
|
|
46
|
+
};
|
|
47
|
+
expect(data).toHaveProperty('status', 'healthy');
|
|
48
|
+
expect(data).toHaveProperty('timestamp');
|
|
49
|
+
expect(data).toHaveProperty('version');
|
|
50
|
+
expect(data).toHaveProperty('service', 'youdotcom-mcp-server');
|
|
51
|
+
expect(typeof data.timestamp).toBe('string');
|
|
52
|
+
expect(typeof data.version).toBe('string');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('mcp endpoint requires authorization header', async () => {
|
|
56
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(response.status).toBe(401);
|
|
65
|
+
expect(response.headers.get('content-type')).toContain('text/plain');
|
|
66
|
+
|
|
67
|
+
const text = await response.text();
|
|
68
|
+
expect(text).toBe('Unauthorized: Authorization header required');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('mcp endpoint requires Bearer token format', async () => {
|
|
72
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
Authorization: 'InvalidFormat token123',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(response.status).toBe(401);
|
|
82
|
+
expect(response.headers.get('content-type')).toContain('text/plain');
|
|
83
|
+
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
expect(text).toBe('Unauthorized: Bearer token required');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('mcp endpoint accepts valid Bearer token', async () => {
|
|
89
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
Accept: 'application/json, text/event-stream',
|
|
94
|
+
Authorization: `Bearer ${testApiKey}`,
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
jsonrpc: '2.0',
|
|
98
|
+
method: 'initialize',
|
|
99
|
+
id: 1,
|
|
100
|
+
params: {
|
|
101
|
+
protocolVersion: '2024-11-05',
|
|
102
|
+
capabilities: {},
|
|
103
|
+
clientInfo: {
|
|
104
|
+
name: 'test-client',
|
|
105
|
+
version: '1.0.0',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(response.status).toBe(200);
|
|
112
|
+
expect(response.headers.get('content-type')).toContain('text/event-stream');
|
|
113
|
+
|
|
114
|
+
// StreamableHTTPTransport uses SSE format, so response will be streaming
|
|
115
|
+
const text = await response.text();
|
|
116
|
+
expect(text).toContain('data:');
|
|
117
|
+
expect(text).toContain('jsonrpc');
|
|
118
|
+
expect(text).toContain('result');
|
|
119
|
+
expect(text).toContain('protocolVersion');
|
|
120
|
+
expect(text).toContain('capabilities');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('mcp endpoint with trailing slash works identically', async () => {
|
|
124
|
+
const response = await fetch(`${baseUrl}/mcp/`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: {
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
Accept: 'application/json, text/event-stream',
|
|
129
|
+
Authorization: `Bearer ${testApiKey}`,
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
jsonrpc: '2.0',
|
|
133
|
+
method: 'initialize',
|
|
134
|
+
id: 1,
|
|
135
|
+
params: {
|
|
136
|
+
protocolVersion: '2024-11-05',
|
|
137
|
+
capabilities: {},
|
|
138
|
+
clientInfo: {
|
|
139
|
+
name: 'test-client',
|
|
140
|
+
version: '1.0.0',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(response.status).toBe(200);
|
|
147
|
+
expect(response.headers.get('content-type')).toContain('text/event-stream');
|
|
148
|
+
|
|
149
|
+
const text = await response.text();
|
|
150
|
+
expect(text).toContain('data:');
|
|
151
|
+
expect(text).toContain('jsonrpc');
|
|
152
|
+
expect(text).toContain('result');
|
|
153
|
+
expect(text).toContain('protocolVersion');
|
|
154
|
+
expect(text).toContain('capabilities');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('mcp endpoint with trailing slash requires authorization', async () => {
|
|
158
|
+
const response = await fetch(`${baseUrl}/mcp/`, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(response.status).toBe(401);
|
|
167
|
+
expect(response.headers.get('content-type')).toContain('text/plain');
|
|
168
|
+
|
|
169
|
+
const text = await response.text();
|
|
170
|
+
expect(text).toBe('Unauthorized: Authorization header required');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('HTTP MCP Endpoint Basic Functionality', () => {
|
|
175
|
+
test('mcp endpoint responds to valid Bearer token', async () => {
|
|
176
|
+
// Test that the endpoint accepts valid Bearer token and doesn't return auth error
|
|
177
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: {
|
|
180
|
+
'Content-Type': 'application/json',
|
|
181
|
+
Accept: 'application/json, text/event-stream',
|
|
182
|
+
Authorization: `Bearer ${testApiKey}`,
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
jsonrpc: '2.0',
|
|
186
|
+
method: 'ping',
|
|
187
|
+
id: 1,
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Should get a response (not 401/403), even if the method isn't supported
|
|
192
|
+
expect(response.status).not.toBe(401);
|
|
193
|
+
expect(response.status).not.toBe(403);
|
|
194
|
+
|
|
195
|
+
// Should be SSE response for StreamableHTTPTransport
|
|
196
|
+
expect(response.headers.get('content-type')).toContain('text/event-stream');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('mcp endpoint processes JSON-RPC requests', async () => {
|
|
200
|
+
// Test basic JSON-RPC structure handling
|
|
201
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: {
|
|
204
|
+
'Content-Type': 'application/json',
|
|
205
|
+
Accept: 'application/json, text/event-stream',
|
|
206
|
+
Authorization: `Bearer ${testApiKey}`,
|
|
207
|
+
},
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
jsonrpc: '2.0',
|
|
210
|
+
method: 'unknown-method',
|
|
211
|
+
id: 123,
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(response.status).toBe(200);
|
|
216
|
+
expect(response.headers.get('content-type')).toContain('text/event-stream');
|
|
217
|
+
|
|
218
|
+
// StreamableHTTPTransport uses SSE format
|
|
219
|
+
const text = await response.text();
|
|
220
|
+
expect(text).toContain('data:');
|
|
221
|
+
expect(text).toContain('jsonrpc');
|
|
222
|
+
expect(text).toContain('123');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('mcp endpoint extracts Bearer token correctly', async () => {
|
|
226
|
+
// Test that different tokens are processed
|
|
227
|
+
const response1 = await fetch(`${baseUrl}/mcp`, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
Accept: 'application/json, text/event-stream',
|
|
232
|
+
Authorization: `Bearer token123`,
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
jsonrpc: '2.0',
|
|
236
|
+
method: 'test',
|
|
237
|
+
id: 1,
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const response2 = await fetch(`${baseUrl}/mcp`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: {
|
|
244
|
+
'Content-Type': 'application/json',
|
|
245
|
+
Accept: 'application/json, text/event-stream',
|
|
246
|
+
Authorization: `Bearer different-token`,
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
jsonrpc: '2.0',
|
|
250
|
+
method: 'test',
|
|
251
|
+
id: 2,
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Both should be processed (not authentication errors)
|
|
256
|
+
expect(response1.status).not.toBe(401);
|
|
257
|
+
expect(response2.status).not.toBe(401);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('mcp endpoint uses StreamableHTTPTransport', async () => {
|
|
261
|
+
// Test that the transport is properly handling requests
|
|
262
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: {
|
|
265
|
+
'Content-Type': 'application/json',
|
|
266
|
+
Accept: 'application/json, text/event-stream',
|
|
267
|
+
Authorization: `Bearer ${testApiKey}`,
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
jsonrpc: '2.0',
|
|
271
|
+
method: 'test',
|
|
272
|
+
id: 42,
|
|
273
|
+
}),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(response.status).toBe(200);
|
|
277
|
+
expect(response.headers.get('content-type')).toContain('text/event-stream');
|
|
278
|
+
|
|
279
|
+
// StreamableHTTPTransport uses SSE format
|
|
280
|
+
const text = await response.text();
|
|
281
|
+
expect(text).toContain('data:');
|
|
282
|
+
expect(text).toContain('jsonrpc');
|
|
283
|
+
expect(text).toContain('42');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('mcp server handles search tool request for latest tech news', async () => {
|
|
287
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: {
|
|
290
|
+
'Content-Type': 'application/json',
|
|
291
|
+
Accept: 'application/json, text/event-stream',
|
|
292
|
+
Authorization: `Bearer ${testApiKey}`,
|
|
293
|
+
},
|
|
294
|
+
body: JSON.stringify({
|
|
295
|
+
jsonrpc: '2.0',
|
|
296
|
+
method: 'tools/call',
|
|
297
|
+
id: 100,
|
|
298
|
+
params: {
|
|
299
|
+
name: 'you-search',
|
|
300
|
+
arguments: {
|
|
301
|
+
query: 'latest tech news',
|
|
302
|
+
count: 3,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(response.status).toBe(200);
|
|
309
|
+
expect(response.headers.get('content-type')).toContain('text/event-stream');
|
|
310
|
+
|
|
311
|
+
const text = await response.text();
|
|
312
|
+
expect(text).toContain('data:');
|
|
313
|
+
expect(text).toContain('jsonrpc');
|
|
314
|
+
expect(text).toContain('result');
|
|
315
|
+
expect(text).toContain('latest tech news');
|
|
316
|
+
expect(text).toContain('Search Results for');
|
|
317
|
+
});
|
|
318
|
+
});
|