mcp_test.rs 15.4 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
376
377
378
379
380
381
382

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

    assert_eq!(brave_search.name, "brave_web_search");
    assert!(brave_search.description.contains("Mock web search"));
    assert_eq!(brave_search.server, "mock_server");
    assert!(brave_search.parameters.is_some());
383
384
}

385
// SSE Parsing Tests (simplified since we don't expose parse_sse_event)
386
387

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

407
408
409
410
411
412
413
414
415
416
    // 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");
417
418
419
420
421
}

// Connection Type Tests

#[tokio::test]
422
423
424
425
426
427
428
429
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()),
        },
430
431
        proxy: None,
        required: false,
432
433
434
435
436
437
438
439
440
441
    };
    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,
        },
442
443
        proxy: None,
        required: false,
444
445
446
447
448
449
450
451
452
453
454
    };
    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(),
        },
455
456
        proxy: None,
        required: false,
457
458
    };
    assert_eq!(stdio_config.name, "stdio_server");
459
460
461
462
463
}

// Integration Pattern Tests

#[tokio::test]
464
async fn test_complete_workflow() {
465
466
    let mock_server = create_mock_server().await;

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

    // 2. Connect to server
485
    let manager = McpManager::with_defaults(config)
486
487
        .await
        .expect("Should connect to mock server");
488

489
490
491
492
    // 3. Verify server connection
    let servers = manager.list_servers();
    assert_eq!(servers.len(), 1);
    assert_eq!(servers[0], "integration_test");
493

494
495
496
    // 4. Check available tools
    let tools = manager.list_tools();
    assert_eq!(tools.len(), 2);
497

498
499
500
501
    // 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"));
502

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

519
520
521
    assert!(result.is_ok(), "Tool execution should succeed");
    let response = result.unwrap();
    assert!(!response.content.is_empty(), "Should return content");
522

523
524
    // 7. Clean shutdown
    manager.shutdown().await;
525
526
527
528
529

    let capabilities = [
        "MCP server initialization",
        "Tool server connection and discovery",
        "Tool availability checking",
530
        "Tool execution",
531
        "Error handling and robustness",
532
533
        "Multi-server support",
        "Schema adaptation",
534
535
536
        "Mock server integration (no external dependencies)",
    ];

537
    assert_eq!(capabilities.len(), 8);
538
}