mcp_test.rs 15.5 KB
Newer Older
1
2
3
// This test suite validates the complete MCP implementation against the
// functionality required for SGLang responses API integration.
//
4
// - Core MCP server functionality
5
6
7
8
9
10
11
// - Tool session management (individual and multi-tool)
// - Tool execution and error handling
// - Schema adaptation and validation
// - Mock server integration for reliable testing

mod common;

12
13
use std::collections::HashMap;

14
15
use common::mock_mcp_server::MockMCPServer;
use serde_json::json;
16
use sglang_router_rs::mcp::{McpConfig, McpError, McpManager, McpServerConfig, McpTransport};
17

18
19
20
21
22
23
24
/// Create a new mock server for testing (each test gets its own)
async fn create_mock_server() -> MockMCPServer {
    MockMCPServer::start()
        .await
        .expect("Failed to start mock MCP server")
}

25
// Core MCP Server Tests
26
27
28

#[tokio::test]
async fn test_mcp_server_initialization() {
29
30
31
32
33
34
35
    let config = McpConfig {
        servers: vec![],
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
    };
36

37
38
39
40
41
42
43
44
45
    // Should succeed but with no connected servers (empty config is allowed)
    let result = McpManager::with_defaults(config).await;
    assert!(result.is_ok(), "Should succeed with empty config");

    let manager = result.unwrap();
    let servers = manager.list_servers();
    assert_eq!(servers.len(), 0, "Should have no servers");
    let tools = manager.list_tools();
    assert_eq!(tools.len(), 0, "Should have no tools");
46
47
48
49
50
51
}

#[tokio::test]
async fn test_server_connection_with_mock() {
    let mock_server = create_mock_server().await;

52
53
54
55
56
57
58
    let config = McpConfig {
        servers: vec![McpServerConfig {
            name: "mock_server".to_string(),
            transport: McpTransport::Streamable {
                url: mock_server.url(),
                token: None,
            },
59
60
            proxy: None,
            required: false,
61
        }],
62
63
64
65
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
66
67
    };

68
    let result = McpManager::with_defaults(config).await;
69
70
    assert!(result.is_ok(), "Should connect to mock server");

71
    let manager = result.unwrap();
72
73
74
75
76
77
78

    let servers = manager.list_servers();
    assert_eq!(servers.len(), 1);
    assert!(servers.contains(&"mock_server".to_string()));

    let tools = manager.list_tools();
    assert_eq!(tools.len(), 2, "Should have 2 tools from mock server");
79

80
81
82
83
    assert!(manager.has_tool("brave_web_search"));
    assert!(manager.has_tool("brave_local_search"));

    manager.shutdown().await;
84
85
86
87
88
89
}

#[tokio::test]
async fn test_tool_availability_checking() {
    let mock_server = create_mock_server().await;

90
91
92
93
94
95
96
    let config = McpConfig {
        servers: vec![McpServerConfig {
            name: "mock_server".to_string(),
            transport: McpTransport::Streamable {
                url: mock_server.url(),
                token: None,
            },
97
98
            proxy: None,
            required: false,
99
        }],
100
101
102
103
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
104
    };
105

106
    let manager = McpManager::with_defaults(config).await.unwrap();
107
108
109

    let test_tools = vec!["brave_web_search", "brave_local_search", "calculator"];
    for tool in test_tools {
110
        let available = manager.has_tool(tool);
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
        match tool {
            "brave_web_search" | "brave_local_search" => {
                assert!(
                    available,
                    "Tool {} should be available from mock server",
                    tool
                );
            }
            "calculator" => {
                assert!(
                    !available,
                    "Tool {} should not be available from mock server",
                    tool
                );
            }
            _ => {}
        }
    }
129
130

    manager.shutdown().await;
131
132
133
}

#[tokio::test]
134
async fn test_multi_server_connection() {
135
136
137
    let mock_server1 = create_mock_server().await;
    let mock_server2 = create_mock_server().await;

138
139
140
141
142
143
144
145
    let config = McpConfig {
        servers: vec![
            McpServerConfig {
                name: "mock_server_1".to_string(),
                transport: McpTransport::Streamable {
                    url: mock_server1.url(),
                    token: None,
                },
146
147
                proxy: None,
                required: false,
148
149
150
151
152
153
154
            },
            McpServerConfig {
                name: "mock_server_2".to_string(),
                transport: McpTransport::Streamable {
                    url: mock_server2.url(),
                    token: None,
                },
155
156
                proxy: None,
                required: false,
157
158
            },
        ],
159
160
161
162
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
163
164
165
166
    };

    // Note: This will fail to connect to both servers in the current implementation
    // since they return the same tools. The manager will connect to the first one.
167
    let result = McpManager::with_defaults(config).await;
168

169
    if let Ok(manager) = result {
170
171
172
173
174
175
176
        let servers = manager.list_servers();
        assert!(!servers.is_empty(), "Should have at least one server");

        let tools = manager.list_tools();
        assert!(tools.len() >= 2, "Should have tools from servers");

        manager.shutdown().await;
177
178
179
180
181
182
183
    }
}

#[tokio::test]
async fn test_tool_execution_with_mock() {
    let mock_server = create_mock_server().await;

184
185
186
187
188
189
190
    let config = McpConfig {
        servers: vec![McpServerConfig {
            name: "mock_server".to_string(),
            transport: McpTransport::Streamable {
                url: mock_server.url(),
                token: None,
            },
191
192
            proxy: None,
            required: false,
193
        }],
194
195
196
197
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
198
    };
199

200
    let manager = McpManager::with_defaults(config).await.unwrap();
201
202

    let result = manager
203
204
        .call_tool(
            "brave_web_search",
205
206
207
208
209
210
211
212
213
            Some(
                json!({
                    "query": "rust programming",
                    "count": 1
                })
                .as_object()
                .unwrap()
                .clone(),
            ),
214
215
216
217
218
219
220
221
222
        )
        .await;

    assert!(
        result.is_ok(),
        "Tool execution should succeed with mock server"
    );

    let response = result.unwrap();
223
224
225
226
227
228
229
230
231
232
    assert!(!response.content.is_empty(), "Should have content");

    // Check the content
    if let rmcp::model::RawContent::Text(text) = &response.content[0].raw {
        assert!(text
            .text
            .contains("Mock search results for: rust programming"));
    } else {
        panic!("Expected text content");
    }
233

234
    manager.shutdown().await;
235
236
237
238
239
240
}

#[tokio::test]
async fn test_concurrent_tool_execution() {
    let mock_server = create_mock_server().await;

241
242
243
244
245
246
247
    let config = McpConfig {
        servers: vec![McpServerConfig {
            name: "mock_server".to_string(),
            transport: McpTransport::Streamable {
                url: mock_server.url(),
                token: None,
            },
248
249
            proxy: None,
            required: false,
250
        }],
251
252
253
254
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
255
256
    };

257
    let manager = McpManager::with_defaults(config).await.unwrap();
258
259

    // Execute tools sequentially (true concurrent execution would require Arc<Mutex>)
260
    let tool_calls = vec![
261
262
        ("brave_web_search", json!({"query": "test1"})),
        ("brave_local_search", json!({"query": "test2"})),
263
264
    ];

265
266
267
268
    for (tool_name, args) in tool_calls {
        let result = manager
            .call_tool(tool_name, Some(args.as_object().unwrap().clone()))
            .await;
269

270
271
272
        assert!(result.is_ok(), "Tool {} should succeed", tool_name);
        let response = result.unwrap();
        assert!(!response.content.is_empty(), "Should have content");
273
    }
274
275

    manager.shutdown().await;
276
277
278
279
280
281
282
283
}

// Error Handling Tests

#[tokio::test]
async fn test_tool_execution_errors() {
    let mock_server = create_mock_server().await;

284
285
286
287
288
289
290
    let config = McpConfig {
        servers: vec![McpServerConfig {
            name: "mock_server".to_string(),
            transport: McpTransport::Streamable {
                url: mock_server.url(),
                token: None,
            },
291
292
            proxy: None,
            required: false,
293
        }],
294
295
296
297
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
298
299
    };

300
    let manager = McpManager::with_defaults(config).await.unwrap();
301
302
303
304
305

    // Try to call unknown tool
    let result = manager
        .call_tool("unknown_tool", Some(serde_json::Map::new()))
        .await;
306
307
    assert!(result.is_err(), "Should fail for unknown tool");

308
309
310
311
312
313
314
315
    match result.unwrap_err() {
        McpError::ToolNotFound(name) => {
            assert_eq!(name, "unknown_tool");
        }
        _ => panic!("Expected ToolNotFound error"),
    }

    manager.shutdown().await;
316
317
318
319
}

#[tokio::test]
async fn test_connection_without_server() {
320
321
322
    let config = McpConfig {
        servers: vec![McpServerConfig {
            name: "nonexistent".to_string(),
323
324
325
326
            transport: McpTransport::Stdio {
                command: "/nonexistent/command".to_string(),
                args: vec![],
                envs: HashMap::new(),
327
            },
328
329
            proxy: None,
            required: false,
330
        }],
331
332
333
334
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
335
336
    };

337
338
339
340
341
342
343
344
345
346
    let result = McpManager::with_defaults(config).await;
    // Manager succeeds but no servers are connected (errors are logged)
    assert!(
        result.is_ok(),
        "Manager should succeed even if servers fail to connect"
    );

    let manager = result.unwrap();
    let servers = manager.list_servers();
    assert_eq!(servers.len(), 0, "Should have no connected servers");
347
348
}

349
// Schema Validation Tests
350
351

#[tokio::test]
352
async fn test_tool_info_structure() {
353
354
    let mock_server = create_mock_server().await;

355
356
357
358
359
360
361
    let config = McpConfig {
        servers: vec![McpServerConfig {
            name: "mock_server".to_string(),
            transport: McpTransport::Streamable {
                url: mock_server.url(),
                token: None,
            },
362
363
            proxy: None,
            required: false,
364
        }],
365
366
367
368
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
369
370
    };

371
    let manager = McpManager::with_defaults(config).await.unwrap();
372
373
374
375

    let tools = manager.list_tools();
    let brave_search = tools
        .iter()
376
        .find(|t| t.name.as_ref() == "brave_web_search")
377
378
        .expect("Should have brave_web_search tool");

379
380
381
382
383
384
385
386
387
    assert_eq!(brave_search.name.as_ref(), "brave_web_search");
    assert!(brave_search
        .description
        .as_ref()
        .map(|d| d.contains("Mock web search"))
        .unwrap_or(false));
    // Note: server information is now maintained separately in the inventory,
    // not in the Tool type itself
    assert!(!brave_search.input_schema.is_empty());
388
389
}

390
// SSE Parsing Tests (simplified since we don't expose parse_sse_event)
391
392

#[tokio::test]
393
async fn test_sse_connection() {
394
    // This tests that SSE configuration is properly handled even when connection fails
395
396
    let config = McpConfig {
        servers: vec![McpServerConfig {
397
398
399
400
401
            name: "sse_test".to_string(),
            transport: McpTransport::Stdio {
                command: "/nonexistent/sse/server".to_string(),
                args: vec!["--sse".to_string()],
                envs: HashMap::new(),
402
            },
403
404
            proxy: None,
            required: false,
405
        }],
406
407
408
409
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
410
411
    };

412
413
414
415
416
417
418
419
420
421
    // Manager succeeds but no servers are connected (errors are logged)
    let result = McpManager::with_defaults(config).await;
    assert!(
        result.is_ok(),
        "Manager should succeed even if SSE server fails to connect"
    );

    let manager = result.unwrap();
    let servers = manager.list_servers();
    assert_eq!(servers.len(), 0, "Should have no connected servers");
422
423
424
425
426
}

// Connection Type Tests

#[tokio::test]
427
428
429
430
431
432
433
434
async fn test_transport_types() {
    // HTTP/Streamable transport
    let http_config = McpServerConfig {
        name: "http_server".to_string(),
        transport: McpTransport::Streamable {
            url: "http://localhost:8080/mcp".to_string(),
            token: Some("auth_token".to_string()),
        },
435
436
        proxy: None,
        required: false,
437
438
439
440
441
442
443
444
445
446
    };
    assert_eq!(http_config.name, "http_server");

    // SSE transport
    let sse_config = McpServerConfig {
        name: "sse_server".to_string(),
        transport: McpTransport::Sse {
            url: "http://localhost:8081/sse".to_string(),
            token: None,
        },
447
448
        proxy: None,
        required: false,
449
450
451
452
453
454
455
456
457
458
459
    };
    assert_eq!(sse_config.name, "sse_server");

    // STDIO transport
    let stdio_config = McpServerConfig {
        name: "stdio_server".to_string(),
        transport: McpTransport::Stdio {
            command: "mcp-server".to_string(),
            args: vec!["--port".to_string(), "8082".to_string()],
            envs: HashMap::new(),
        },
460
461
        proxy: None,
        required: false,
462
463
    };
    assert_eq!(stdio_config.name, "stdio_server");
464
465
466
467
468
}

// Integration Pattern Tests

#[tokio::test]
469
async fn test_complete_workflow() {
470
471
    let mock_server = create_mock_server().await;

472
473
474
475
476
477
478
479
    // 1. Initialize configuration
    let config = McpConfig {
        servers: vec![McpServerConfig {
            name: "integration_test".to_string(),
            transport: McpTransport::Streamable {
                url: mock_server.url(),
                token: None,
            },
480
481
            proxy: None,
            required: false,
482
        }],
483
484
485
486
        pool: Default::default(),
        proxy: None,
        warmup: Vec::new(),
        inventory: Default::default(),
487
488
489
    };

    // 2. Connect to server
490
    let manager = McpManager::with_defaults(config)
491
492
        .await
        .expect("Should connect to mock server");
493

494
495
496
497
    // 3. Verify server connection
    let servers = manager.list_servers();
    assert_eq!(servers.len(), 1);
    assert_eq!(servers[0], "integration_test");
498

499
500
501
    // 4. Check available tools
    let tools = manager.list_tools();
    assert_eq!(tools.len(), 2);
502

503
504
505
506
    // 5. Verify specific tools exist
    assert!(manager.has_tool("brave_web_search"));
    assert!(manager.has_tool("brave_local_search"));
    assert!(!manager.has_tool("nonexistent_tool"));
507

508
509
    // 6. Execute a tool
    let result = manager
510
511
        .call_tool(
            "brave_web_search",
512
513
514
515
516
517
518
519
520
            Some(
                json!({
                    "query": "SGLang router MCP integration",
                    "count": 1
                })
                .as_object()
                .unwrap()
                .clone(),
            ),
521
522
523
        )
        .await;

524
525
526
    assert!(result.is_ok(), "Tool execution should succeed");
    let response = result.unwrap();
    assert!(!response.content.is_empty(), "Should return content");
527

528
529
    // 7. Clean shutdown
    manager.shutdown().await;
530
531
532
533
534

    let capabilities = [
        "MCP server initialization",
        "Tool server connection and discovery",
        "Tool availability checking",
535
        "Tool execution",
536
        "Error handling and robustness",
537
538
        "Multi-server support",
        "Schema adaptation",
539
540
541
        "Mock server integration (no external dependencies)",
    ];

542
    assert_eq!(capabilities.len(), 8);
543
}