Commit 51fbbf9
Changed files (3)
pkg/mcp/resources_test.go
@@ -0,0 +1,127 @@
+package mcp
+
+import (
+ "testing"
+)
+
+func TestResource_Creation(t *testing.T) {
+ resource := Resource{
+ URI: "file:///home/user/document.txt",
+ Name: "document.txt",
+ Description: "A sample text document",
+ MimeType: "text/plain",
+ }
+
+ if resource.URI != "file:///home/user/document.txt" {
+ t.Errorf("Expected URI 'file:///home/user/document.txt', got %s", resource.URI)
+ }
+ if resource.Name != "document.txt" {
+ t.Errorf("Expected name 'document.txt', got %s", resource.Name)
+ }
+ if resource.MimeType != "text/plain" {
+ t.Errorf("Expected mime type 'text/plain', got %s", resource.MimeType)
+ }
+}
+
+func TestListResourcesResult_Empty(t *testing.T) {
+ result := ListResourcesResult{
+ Resources: []Resource{},
+ }
+
+ if len(result.Resources) != 0 {
+ t.Errorf("Expected 0 resources, got %d", len(result.Resources))
+ }
+}
+
+func TestListResourcesResult_WithResources(t *testing.T) {
+ resources := []Resource{
+ {URI: "file:///file1.txt", Name: "file1.txt"},
+ {URI: "git://repo/branch/file.go", Name: "file.go"},
+ {URI: "memory://entity/123", Name: "Entity 123"},
+ }
+
+ result := ListResourcesResult{
+ Resources: resources,
+ }
+
+ if len(result.Resources) != 3 {
+ t.Errorf("Expected 3 resources, got %d", len(result.Resources))
+ }
+ if result.Resources[0].URI != "file:///file1.txt" {
+ t.Errorf("Expected first resource URI 'file:///file1.txt', got %s", result.Resources[0].URI)
+ }
+}
+
+func TestReadResourceRequest_Creation(t *testing.T) {
+ req := ReadResourceRequest{
+ URI: "file:///path/to/resource",
+ }
+
+ if req.URI != "file:///path/to/resource" {
+ t.Errorf("Expected URI 'file:///path/to/resource', got %s", req.URI)
+ }
+}
+
+func TestReadResourceResult_WithContent(t *testing.T) {
+ contents := []Content{
+ NewTextContent("File content here"),
+ }
+
+ result := ReadResourceResult{
+ Contents: contents,
+ }
+
+ if len(result.Contents) != 1 {
+ t.Errorf("Expected 1 content item, got %d", len(result.Contents))
+ }
+
+ if textContent, ok := result.Contents[0].(TextContent); ok {
+ if textContent.Text != "File content here" {
+ t.Errorf("Expected content 'File content here', got %s", textContent.Text)
+ }
+ } else {
+ t.Error("Expected TextContent type")
+ }
+}
+
+func TestResourceSchemes(t *testing.T) {
+ testCases := []struct {
+ name string
+ uri string
+ expected string
+ }{
+ {"file scheme", "file:///home/user/doc.txt", "file"},
+ {"git scheme", "git://repo/main/src/file.go", "git"},
+ {"memory scheme", "memory://graph/entity/123", "memory"},
+ {"http scheme", "http://example.com/resource", "http"},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ resource := Resource{
+ URI: tc.uri,
+ Name: "Test Resource",
+ }
+
+ // Extract scheme from URI
+ scheme := ""
+ if idx := indexOf(resource.URI, "://"); idx != -1 {
+ scheme = resource.URI[:idx]
+ }
+
+ if scheme != tc.expected {
+ t.Errorf("Expected scheme '%s', got '%s'", tc.expected, scheme)
+ }
+ })
+ }
+}
+
+// Helper function for testing
+func indexOf(s, substr string) int {
+ for i := 0; i <= len(s)-len(substr); i++ {
+ if s[i:i+len(substr)] == substr {
+ return i
+ }
+ }
+ return -1
+}
\ No newline at end of file
pkg/mcp/server.go
@@ -18,9 +18,10 @@ type Server struct {
// Handler functions
toolHandlers map[string]ToolHandler
- promptHandlers map[string]PromptHandler
- promptDefinitions map[string]Prompt
- resourceHandlers map[string]ResourceHandler
+ promptHandlers map[string]PromptHandler
+ promptDefinitions map[string]Prompt
+ resourceHandlers map[string]ResourceHandler
+ resourceDefinitions map[string]Resource
// Lifecycle handlers
initializeHandler func(InitializeRequest) (InitializeResult, error)
@@ -39,10 +40,11 @@ func NewServer(name, version string) *Server {
return &Server{
name: name,
version: version,
- toolHandlers: make(map[string]ToolHandler),
- promptHandlers: make(map[string]PromptHandler),
- promptDefinitions: make(map[string]Prompt),
- resourceHandlers: make(map[string]ResourceHandler),
+ toolHandlers: make(map[string]ToolHandler),
+ promptHandlers: make(map[string]PromptHandler),
+ promptDefinitions: make(map[string]Prompt),
+ resourceHandlers: make(map[string]ResourceHandler),
+ resourceDefinitions: make(map[string]Resource),
capabilities: ServerCapabilities{
Tools: &ToolsCapability{},
Prompts: &PromptsCapability{},
@@ -67,11 +69,21 @@ func (s *Server) RegisterPrompt(prompt Prompt, handler PromptHandler) {
s.promptDefinitions[prompt.Name] = prompt
}
-// RegisterResource registers a resource handler
+// RegisterResource registers a resource handler with minimal definition
func (s *Server) RegisterResource(uri string, handler ResourceHandler) {
+ resource := Resource{
+ URI: uri,
+ Name: extractResourceName(uri),
+ }
+ s.RegisterResourceWithDefinition(resource, handler)
+}
+
+// RegisterResourceWithDefinition registers a resource with its full definition and handler
+func (s *Server) RegisterResourceWithDefinition(resource Resource, handler ResourceHandler) {
s.mu.Lock()
defer s.mu.Unlock()
- s.resourceHandlers[uri] = handler
+ s.resourceHandlers[resource.URI] = handler
+ s.resourceDefinitions[resource.URI] = resource
}
// SetInitializeHandler sets the initialize handler
@@ -119,6 +131,19 @@ func (s *Server) ListPrompts() []Prompt {
return prompts
}
+// ListResources returns all registered resources
+func (s *Server) ListResources() []Resource {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ resources := make([]Resource, 0, len(s.resourceDefinitions))
+ for _, resource := range s.resourceDefinitions {
+ resources = append(resources, resource)
+ }
+
+ return resources
+}
+
// Run starts the server and handles JSON-RPC over stdio
func (s *Server) Run(ctx context.Context) error {
scanner := bufio.NewScanner(os.Stdin)
@@ -274,8 +299,8 @@ func (s *Server) handleGetPrompt(req JSONRPCRequest) JSONRPCResponse {
}
func (s *Server) handleListResources(req JSONRPCRequest) JSONRPCResponse {
- // Return empty resources list for now
- result := ListResourcesResult{Resources: []Resource{}}
+ resources := s.ListResources()
+ result := ListResourcesResult{Resources: resources}
return s.createSuccessResponse(req.ID, result)
}
@@ -354,3 +379,15 @@ func NewToolError(message string) CallToolResult {
IsError: true,
}
}
+
+// Helper function to extract resource name from URI
+func extractResourceName(uri string) string {
+ // Find the last "/" in the URI and extract the part after it
+ for i := len(uri) - 1; i >= 0; i-- {
+ if uri[i] == '/' {
+ return uri[i+1:]
+ }
+ }
+ // If no "/" found, return the entire URI
+ return uri
+}
pkg/mcp/server_resources_test.go
@@ -0,0 +1,117 @@
+package mcp
+
+import (
+ "testing"
+)
+
+func TestServer_RegisterResource(t *testing.T) {
+ server := NewServer("test-server", "1.0.0")
+
+ // Create a test resource handler
+ handler := func(req ReadResourceRequest) (ReadResourceResult, error) {
+ return ReadResourceResult{
+ Contents: []Content{
+ NewTextContent("Test resource content"),
+ },
+ }, nil
+ }
+
+ // Register resource
+ server.RegisterResource("file:///test/resource.txt", handler)
+
+ // Test that resource is registered (we'll need to add ListResources method)
+ resources := server.ListResources()
+ if len(resources) != 1 {
+ t.Errorf("Expected 1 resource, got %d", len(resources))
+ }
+
+ if resources[0].URI != "file:///test/resource.txt" {
+ t.Errorf("Expected resource URI 'file:///test/resource.txt', got %s", resources[0].URI)
+ }
+}
+
+func TestServer_ListResources_Empty(t *testing.T) {
+ server := NewServer("test-server", "1.0.0")
+
+ resources := server.ListResources()
+ if len(resources) != 0 {
+ t.Errorf("Expected 0 resources, got %d", len(resources))
+ }
+}
+
+func TestServer_MultipleResources(t *testing.T) {
+ server := NewServer("test-server", "1.0.0")
+
+ handler := func(req ReadResourceRequest) (ReadResourceResult, error) {
+ return ReadResourceResult{}, nil
+ }
+
+ // Register multiple resources
+ server.RegisterResource("file:///file1.txt", handler)
+ server.RegisterResource("git://repo/main/file.go", handler)
+ server.RegisterResource("memory://entity/123", handler)
+
+ resources := server.ListResources()
+ if len(resources) != 3 {
+ t.Errorf("Expected 3 resources, got %d", len(resources))
+ }
+
+ // Check that all URIs are present (order may vary due to map iteration)
+ uris := make(map[string]bool)
+ for _, resource := range resources {
+ uris[resource.URI] = true
+ }
+
+ expectedURIs := []string{
+ "file:///file1.txt",
+ "git://repo/main/file.go",
+ "memory://entity/123",
+ }
+
+ for _, expectedURI := range expectedURIs {
+ if !uris[expectedURI] {
+ t.Errorf("Expected URI %s not found in resources", expectedURI)
+ }
+ }
+}
+
+func TestServer_RegisterResourceWithDefinition(t *testing.T) {
+ server := NewServer("test-server", "1.0.0")
+
+ resource := Resource{
+ URI: "file:///docs/readme.md",
+ Name: "README",
+ Description: "Project documentation",
+ MimeType: "text/markdown",
+ }
+
+ handler := func(req ReadResourceRequest) (ReadResourceResult, error) {
+ return ReadResourceResult{
+ Contents: []Content{
+ NewTextContent("# Project Documentation\n\nThis is the README."),
+ },
+ }, nil
+ }
+
+ // Register resource with full definition
+ server.RegisterResourceWithDefinition(resource, handler)
+
+ resources := server.ListResources()
+ if len(resources) != 1 {
+ t.Errorf("Expected 1 resource, got %d", len(resources))
+ }
+
+ res := resources[0]
+ if res.URI != "file:///docs/readme.md" {
+ t.Errorf("Expected URI 'file:///docs/readme.md', got %s", res.URI)
+ }
+ if res.Name != "README" {
+ t.Errorf("Expected name 'README', got %s", res.Name)
+ }
+ if res.Description != "Project documentation" {
+ t.Errorf("Expected description 'Project documentation', got %s", res.Description)
+ }
+ if res.MimeType != "text/markdown" {
+ t.Errorf("Expected mime type 'text/markdown', got %s", res.MimeType)
+ }
+}
\ No newline at end of file