|
6 | 6 | "strings" |
7 | 7 | "testing" |
8 | 8 |
|
| 9 | + "skillful-mcp/internal/config" |
9 | 10 | "skillful-mcp/internal/mcpserver" |
10 | 11 |
|
11 | 12 | "github.com/modelcontextprotocol/go-sdk/mcp" |
@@ -521,3 +522,134 @@ func TestE2EStructuredOutput(t *testing.T) { |
521 | 522 | } |
522 | 523 | }) |
523 | 524 | } |
| 525 | + |
| 526 | +func TestE2EServerOptions(t *testing.T) { |
| 527 | + t.Parallel() |
| 528 | + ctx := t.Context() |
| 529 | + |
| 530 | + // Downstream server has 3 tools and 2 resources. |
| 531 | + session := startFakeServer(t, ctx, "Original description", |
| 532 | + []mcp.Tool{ |
| 533 | + {Name: "allowed_tool", Description: "This tool is allowed"}, |
| 534 | + {Name: "blocked_tool", Description: "This tool is blocked"}, |
| 535 | + {Name: "another_allowed", Description: "Also allowed"}, |
| 536 | + }, |
| 537 | + []mcp.Resource{ |
| 538 | + {URI: "file:///allowed.txt", Name: "allowed.txt", Description: "Allowed resource"}, |
| 539 | + {URI: "file:///blocked.txt", Name: "blocked.txt", Description: "Blocked resource"}, |
| 540 | + }, |
| 541 | + ) |
| 542 | + |
| 543 | + srv, err := mcpserver.NewServerFromSession(ctx, session, config.ServerOptions{ |
| 544 | + Description: "Custom skill description", |
| 545 | + AllowedTools: []string{"allowed_tool", "another_allowed"}, |
| 546 | + AllowedResources: []string{"file:///allowed.txt"}, |
| 547 | + }) |
| 548 | + if err != nil { |
| 549 | + t.Fatal(err) |
| 550 | + } |
| 551 | + mgr, err := mcpserver.NewManagerFromServers(map[string]*mcpserver.Server{"filtered": srv}) |
| 552 | + if err != nil { |
| 553 | + t.Fatal(err) |
| 554 | + } |
| 555 | + defer mgr.Close() |
| 556 | + |
| 557 | + upstream := connectTestClient(t, ctx, mgr) |
| 558 | + |
| 559 | + t.Run("list_skills_shows_custom_description", func(t *testing.T) { |
| 560 | + result, err := upstream.CallTool(ctx, &mcp.CallToolParams{Name: "list_skills"}) |
| 561 | + if err != nil { |
| 562 | + t.Fatal(err) |
| 563 | + } |
| 564 | + tc := result.Content[0].(*mcp.TextContent) |
| 565 | + if !strings.Contains(tc.Text, "Custom skill description") { |
| 566 | + t.Errorf("expected custom description, got %q", tc.Text) |
| 567 | + } |
| 568 | + if strings.Contains(tc.Text, "Original description") { |
| 569 | + t.Error("original description should be overridden") |
| 570 | + } |
| 571 | + }) |
| 572 | + |
| 573 | + t.Run("use_skill_shows_only_allowed_tools", func(t *testing.T) { |
| 574 | + result, err := upstream.CallTool(ctx, &mcp.CallToolParams{ |
| 575 | + Name: "use_skill", |
| 576 | + Arguments: map[string]any{"skill_name": "filtered"}, |
| 577 | + }) |
| 578 | + if err != nil { |
| 579 | + t.Fatal(err) |
| 580 | + } |
| 581 | + tc := result.Content[0].(*mcp.TextContent) |
| 582 | + if !strings.Contains(tc.Text, "allowed_tool(") { |
| 583 | + t.Errorf("expected allowed_tool signature, got %q", tc.Text) |
| 584 | + } |
| 585 | + if !strings.Contains(tc.Text, "another_allowed(") { |
| 586 | + t.Errorf("expected another_allowed signature, got %q", tc.Text) |
| 587 | + } |
| 588 | + if strings.Contains(tc.Text, "blocked_tool") { |
| 589 | + t.Error("blocked_tool should not appear") |
| 590 | + } |
| 591 | + }) |
| 592 | + |
| 593 | + t.Run("use_skill_shows_only_allowed_resources", func(t *testing.T) { |
| 594 | + result, err := upstream.CallTool(ctx, &mcp.CallToolParams{ |
| 595 | + Name: "use_skill", |
| 596 | + Arguments: map[string]any{"skill_name": "filtered"}, |
| 597 | + }) |
| 598 | + if err != nil { |
| 599 | + t.Fatal(err) |
| 600 | + } |
| 601 | + tc := result.Content[0].(*mcp.TextContent) |
| 602 | + if !strings.Contains(tc.Text, "file:///allowed.txt") { |
| 603 | + t.Errorf("expected allowed resource, got %q", tc.Text) |
| 604 | + } |
| 605 | + if strings.Contains(tc.Text, "file:///blocked.txt") { |
| 606 | + t.Error("blocked resource should not appear") |
| 607 | + } |
| 608 | + }) |
| 609 | + |
| 610 | + t.Run("execute_code_can_call_allowed_tool", func(t *testing.T) { |
| 611 | + code := `allowed_tool()` |
| 612 | + result, err := upstream.CallTool(ctx, &mcp.CallToolParams{ |
| 613 | + Name: "execute_code", |
| 614 | + Arguments: map[string]any{"code": code}, |
| 615 | + }) |
| 616 | + if err != nil { |
| 617 | + t.Fatal(err) |
| 618 | + } |
| 619 | + if result.IsError { |
| 620 | + tc := result.Content[0].(*mcp.TextContent) |
| 621 | + t.Fatalf("error: %s", tc.Text) |
| 622 | + } |
| 623 | + }) |
| 624 | + |
| 625 | + t.Run("execute_code_cannot_call_blocked_tool", func(t *testing.T) { |
| 626 | + code := `blocked_tool()` |
| 627 | + result, err := upstream.CallTool(ctx, &mcp.CallToolParams{ |
| 628 | + Name: "execute_code", |
| 629 | + Arguments: map[string]any{"code": code}, |
| 630 | + }) |
| 631 | + if err != nil { |
| 632 | + t.Fatal(err) |
| 633 | + } |
| 634 | + if !result.IsError { |
| 635 | + t.Error("expected error calling blocked tool") |
| 636 | + } |
| 637 | + }) |
| 638 | + |
| 639 | + t.Run("blocked_tool_does_not_cause_prefix", func(t *testing.T) { |
| 640 | + // If blocked_tool were visible, tools with same name in other |
| 641 | + // servers would get prefixed. Since it's filtered, no prefix needed. |
| 642 | + result, err := upstream.CallTool(ctx, &mcp.CallToolParams{ |
| 643 | + Name: "use_skill", |
| 644 | + Arguments: map[string]any{"skill_name": "filtered"}, |
| 645 | + }) |
| 646 | + if err != nil { |
| 647 | + t.Fatal(err) |
| 648 | + } |
| 649 | + tc := result.Content[0].(*mcp.TextContent) |
| 650 | + // Tool names should not have "filtered_" prefix since there's no conflict. |
| 651 | + if strings.Contains(tc.Text, "filtered_allowed_tool") { |
| 652 | + t.Error("tools should not be prefixed when no conflict exists") |
| 653 | + } |
| 654 | + }) |
| 655 | +} |
0 commit comments