developer-guide.md 28 KB

MCP Vala Library Developer Guide

This guide provides comprehensive information for developers who want to use the MCP Vala library to create Model Context Protocol (MCP) servers.

Table of Contents

  1. Architecture Overview
  2. Getting Started
  3. Core Concepts
  4. Implementing Resources
  5. Implementing Tools
  6. Implementing Prompts
  7. Best Practices
  8. Error Handling
  9. Testing and Debugging
  10. Extending the Library
  11. Troubleshooting

Architecture Overview

The MCP Vala library follows a layered architecture that separates concerns and provides clean interfaces for extensibility:

┌─────────────────────────────────────────────────────────────┐
│                   Application Layer                    │
│  ┌─────────────────────────────────────────────┐    │
│  │            Your Server Code             │    │
│  └─────────────────────────────────────────────┘    │
│                   ↓                               │
│  ┌─────────────────────────────────────────────┐    │
│  │         MCP Vala Library              │    │
│  │  ┌─────────────────────────────┐      │    │
│  │  │    Core Components       │      │    │
│  │  │  • Server               │      │    │
│  │  │  • Jsonrpc.Server       │      │    │
│  │  └─────────────────────────────┘      │    │
│  │  ┌─────────────────────────────┐      │    │
│  │  │    Managers             │      │    │
│  │  │  • ResourceManager    │      │    │
│  │  │  • ToolManager         │      │    │
│  │  │  • PromptManager       │      │    │
│  │  └─────────────────────────────┘      │    │
│  │  ┌─────────────────────────────┐      │    │
│  │  │    Base Classes         │      │    │
│  │  │  • BaseExecutor        │      │    │
│  │  │  • BaseProvider        │      │    │
│  │  │  • BaseTemplate        │      │    │
│  │  └─────────────────────────────┘      │    │
│  └─────────────────────────────────────────────┘    │
│                   ↓                               │
│  ┌─────────────────────────────────────────────┐    │
│  │        jsonrpc-glib-1.0 Library          │    │
│  │  • Jsonrpc.Server               │    │
│  │  • Jsonrpc.Client               │    │
│  │  • JSON-RPC 2.0 Protocol      │    │
│  │  • STDIO Transport              │    │
│  └─────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────┘

Key Components

  • Core.Server: Main server class that orchestrates all MCP functionality using Jsonrpc.Server
  • Jsonrpc.Server: Standard JSON-RPC 2.0 server implementation from jsonrpc-glib-1.0
  • Managers: Coordinate resources, tools, and prompts
  • Base Classes: Provide common functionality for custom implementations

Getting Started

Prerequisites

  • Vala compiler (valac) >= 0.56
  • Required dependencies:
    • glib-2.0 >= 2.70
    • gobject-2.0 >= 2.70
    • gio-2.0 >= 2.70
    • json-glib-1.0 >= 1.6
    • jsonrpc-glib-1.0 >= 3.34
    • gio-unix-2.0 (for STDIO transport)
    • gee-0.8 (for collections)

Basic Server Setup

using Mcp;

public class MyServer : GLib.Object {
    private Mcp.Core.Server server;
    
    public MyServer () {
        // Create server information
        var server_info = new Mcp.Types.Protocol.ServerInfo (
            "my-server",
            "1.0.0",
            "My custom MCP server",
            "https://my-server.example.com"
        );
        
        // Create capabilities
        var capabilities = new Mcp.Types.Protocol.ServerCapabilities ();
        capabilities.logging = true;
        capabilities.completions = new Mcp.Types.Protocol.CompletionsCapabilities ();
        
        // Create server
        server = new Mcp.Core.Server (server_info, capabilities);
    }
    
    public async int run () {
        // Start the server
        bool started = yield server.start ();
        if (!started) {
            stderr.printf ("Failed to start server\n");
            return 1;
        }
        
        // Run main loop
        var main_loop = new MainLoop ();
        main_loop.run ();
        
        return 0;
    }
}

int main (string[] args) {
    var server = new MyServer ();
    return server.run ();
}

Core Concepts

Resources

Resources represent data that can be read by clients:

// Resource definition
var resource = new Mcp.Resources.Types.Resource (
    "file:///path/to/file.txt",
    "My File"
);
resource.description = "A sample text file";
resource.mime_type = "text/plain";

// Resource contents
var contents = new Mcp.Types.Common.TextResourceContents (
    resource.uri,
    "Hello, World!"
);

Tools

Tools are functions that clients can execute:

// Tool definition
var input_schema = Mcp.Types.VariantUtils.new_dict_builder ();
input_schema.add ("{sv}", "type", new Variant.string ("object"));

var definition = new Mcp.Tools.Types.ToolDefinition (
    "my_tool",
    input_schema.end ()
);

// Tool execution result
var content = new Mcp.Types.Common.TextContent ("Operation completed");
var contents = new Gee.ArrayList<Mcp.Types.Common.ContentBlock> ();
contents.add (content);
var result = new Mcp.Tools.Types.CallToolResult (contents);

Prompts

Prompts are templates for generating consistent messages:

// Prompt definition
var prompt = new Mcp.Prompts.Types.PromptDefinition ("my_prompt");
prompt.description = "A custom prompt template";

// Prompt result
var result = new Mcp.Prompts.Types.GetPromptResult ();
var message = new Mcp.Prompts.Types.PromptMessage (
    "user",
    new Mcp.Types.Common.TextContent ("Generated prompt text")
);
result.messages.add (message);

Implementing Resources

Creating a Resource Provider

Implement the Mcp.Resources.Provider interface or extend Mcp.Resources.BaseProvider:

public class MyResourceProvider : Mcp.Resources.BaseProvider {
    private HashTable<string, string> data_store;
    
    public MyResourceProvider () {
        data_store = new HashTable<string, string> (str_hash, str_equal);
        // Initialize with some data
        data_store.insert ("my://item1", "Content of item 1");
        data_store.insert ("my://item2", "Content of item 2");
    }
    
    public override async Gee.ArrayList<Mcp.Resources.Types.Resource> list_resources (string? cursor) throws Error {
        var resources = new Gee.ArrayList<Mcp.Resources.Types.Resource> ();
        
        foreach (var uri in data_store.get_keys ()) {
            var resource = new Mcp.Resources.Types.Resource (uri, uri);
            resource.mime_type = "text/plain";
            resource.size = data_store[uri].length;
            resources.add (resource);
        }
        
        return resources;
    }
    
    public override async Mcp.Types.Common.ResourceContents read_resource (string uri) throws Error {
        if (!data_store.contains (uri)) {
            throw new Mcp.Core.Error.NOT_FOUND ("Resource not found: %s".printf (uri));
        }
        
        return new Mcp.Types.Common.TextResourceContents (uri, data_store[uri]);
    }
}

// Register with server
server.resource_manager.register_provider ("my_provider", new MyResourceProvider ());

Resource Subscription Support

Enable clients to receive updates when resources change:

public override async void subscribe (string uri) throws Error {
    // Track subscription
    yield base.subscribe (uri);
    
    // Emit update when data changes
    notify_resource_updated (uri);
}

Implementing Tools

Creating a Tool Executor

Implement the Mcp.Tools.Executor interface or extend Mcp.Tools.BaseExecutor:

public class MyTool : Mcp.Tools.BaseExecutor {
    public MyTool () {
        // Define input schema
        var input_schema = Mcp.Types.VariantUtils.new_dict_builder ();
        input_schema.add ("{sv}", "type", new Variant.string ("object"));
        
        var properties = Mcp.Types.VariantUtils.new_dict_builder ();
        var text_prop = Mcp.Types.VariantUtils.new_dict_builder ();
        text_prop.add ("{sv}", "type", new Variant.string ("string"));
        text_prop.add ("{sv}", "description", new Variant.string ("Text to process"));
        properties.add ("{sv}", "text", text_prop.end ());
        
        var required = new VariantBuilder (new VariantType ("as"));
        required.add_value ("s", "text");
        input_schema.add ("{sv}", "properties", properties.end ());
        input_schema.add ("{sv}", "required", required.end ());
        
        var definition = new Mcp.Tools.Types.ToolDefinition ("my_tool", input_schema.end ());
        definition.description = "Processes text input";
        
        base (definition);
    }
    
    protected override async Mcp.Tools.Types.CallToolResult do_execute (Variant arguments) throws Error {
        string text = get_string_arg (arguments, "text");
        
        // Process the text
        string processed = text.up ();
        
        return create_text_result ("Processed: %s".printf (processed));
    }
}

// Register with server
server.tool_manager.register_executor ("my_tool", new MyTool ());

Advanced Tool Features

Structured Results

Return structured data with your results:

var structured_data = Mcp.Types.VariantUtils.new_dict_builder ();
structured_data.add ("{sv}", "original", new Variant.string (text));
structured_data.add ("{sv}", "processed", new Variant.string (processed));
structured_data.add ("{sv}", "length", new Variant.int32 (processed.length));

return create_structured_result ("Text processed successfully", structured_data.end ());

Error Handling

Handle errors gracefully:

protected override async Mcp.Tools.Types.CallToolResult do_execute (Variant arguments) throws Error {
    try {
        // Validate input
        string text = get_string_arg (arguments, "text");
        if (text.length == 0) {
            throw new Mcp.Core.Error.INVALID_PARAMS ("Text cannot be empty");
        }
        
        // Process
        string result = process_text (text);
        return create_text_result (result);
        
    } catch (Error e) {
        // Return error result
        return create_error_result (e);
    }
}

Implementing Prompts

Creating a Prompt Template

Implement the Mcp.Prompts.Template interface or extend Mcp.Prompts.BaseTemplate:

public class MyPrompt : Mcp.Prompts.BaseTemplate {
    public MyPrompt () {
        base ("my_prompt", "My Prompt", "A custom prompt template");
        
        // Add arguments
        add_argument ("topic", "Topic", "The topic to generate content for", true);
        add_argument ("style", "Style", "Writing style (formal, casual, creative)", false);
        add_argument ("length", "Length", "Desired length (short, medium, long)", false);
    }
    
    protected override async Mcp.Prompts.Types.GetPromptResult do_render (Variant arguments) throws Error {
        var messages = new Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> ();
        
        string topic = Mcp.Types.VariantUtils.get_string (arguments, "topic");
        string? style = Mcp.Types.VariantUtils.get_string (arguments, "style", "neutral");
        string? length = Mcp.Types.VariantUtils.get_string (arguments, "length", "medium");
        
        string template = """Generate {{style}} content about {{topic}}.
Target length: {{length}}.

Please provide:
1. Introduction to the topic
2. Key points about {{topic}}
3. Analysis or insights
4. Conclusion or summary""";
        
        messages.add (create_user_message (template, arguments));
        return messages;
    }
    
    private string? get_string_arg (Variant arguments, string name, string? default_value = null) {
        return Mcp.Types.VariantUtils.get_string (arguments, name, default_value);
    }
}

// Register with server
server.prompt_manager.register_template ("my_prompt", new MyPrompt ());

Advanced Prompt Features

Conditional Content

Use conditional blocks in templates:

string template = """Generate content about {{topic}}.

{{#formal}}
Write in a formal, professional tone suitable for business communication.
{{/formal}}

{{#casual}}
Write in a relaxed, conversational tone suitable for informal settings.
{{/casual}}

{{#creative}}
Use creative language and imaginative expressions.
{{/creative}}""";

Multi-Message Prompts

Create conversations with multiple messages:

protected override Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> generate_messages (Variant arguments) {
        var messages = new Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> ();
        
        // System message
        var system_content = new Mcp.Types.Common.TextContent (
            "You are a helpful assistant specializing in %s.".printf (
                Mcp.Types.VariantUtils.get_string (arguments, "topic")
            )
        );
        var system_msg = new Mcp.Prompts.Types.PromptMessage ("system", system_content);
        messages.add (system_msg);
        
        // User message
        var user_content = new Mcp.Types.Common.TextContent (
            "Tell me about %s.".printf (
                Mcp.Types.VariantUtils.get_string (arguments, "topic")
            )
        );
        var user_msg = new Mcp.Prompts.Types.PromptMessage ("user", user_content);
        messages.add (user_msg);
        
        return messages;
    }

Best Practices

Server Initialization

  1. Set up capabilities properly:

    var capabilities = new Mcp.Types.Protocol.ServerCapabilities ();
    capabilities.logging = true;  // Always enable logging
    
  2. Handle initialization errors:

    try {
       server = new Mcp.Core.Server (server_info, capabilities);
    } catch (Error e) {
       stderr.printf ("Failed to create server: %s\n", e.message);
       return;
    }
    
  3. Register components before starting:

    // Register all providers, executors, and templates
    setup_resources ();
    setup_tools ();
    setup_prompts ();
       
    // Then start the server
    yield server.start ();
    

Resource Management

  1. Use appropriate MIME types:

    resource.mime_type = "text/plain";           // Text files
    resource.mime_type = "application/json";      // JSON data
    resource.mime_type = "image/png";           // Images
    
  2. Handle large resources efficiently:

    public override async Mcp.Types.Common.ResourceContents read_resource (string uri) throws Error {
       // Check size before loading
       var metadata = get_resource_metadata (uri);
       if (metadata != null && metadata.size > 1024 * 1024) {  // 1MB
           // Consider streaming for large files
           return yield read_large_resource_streaming (uri);
       }
           
       // Normal loading for smaller files
       return yield read_resource_normal (uri);
    }
    
  3. Implement subscription notifications:

    // When data changes
    data_store.insert (uri, new_content);
       
    // Notify all subscribers
    notify_resource_updated (uri);
       
    // Trigger list change notification
    list_changed ();
    

Tool Implementation

  1. Validate all inputs:

    protected override void validate_arguments (Variant arguments) throws Error {
           // Call base validation
           base.validate_arguments (arguments);
               
           // Add custom validation
           if (Mcp.Types.VariantUtils.has_key (arguments, "custom_param")) {
               string value = Mcp.Types.VariantUtils.get_string (arguments, "custom_param");
               if (!is_valid_custom_param (value)) {
                   throw new Mcp.Core.Error.INVALID_PARAMS ("Invalid custom_param");
               }
           }
       }
    
  2. Use structured results:

    var structured_data = Mcp.Types.VariantUtils.new_dict_builder ();
    structured_data.add ("{sv}", "status", new Variant.string ("success"));
    structured_data.add ("{sv}", "result", result_object);
    structured_data.add ("{sv}", "duration_ms", new Variant.double (elapsed_ms));
       
    return create_structured_result ("Operation completed", structured_data.end ());
    
  3. Handle long-running operations:

    protected override async Mcp.Tools.Types.CallToolResult do_execute (Variant arguments) throws Error {
           // Start progress notification
           var progress = new Mcp.Tools.Types.ToolProgress (0.0, "Processing...");
           report_progress (progress);
               
           // Perform long operation
           var result = yield perform_long_operation (arguments);
               
           // Final progress
           var final_progress = new Mcp.Tools.Types.ToolProgress (1.0, "Completed");
           report_progress (final_progress);
               
           return result;
       }
    

Prompt Design

  1. Keep templates focused:

    // Good: Specific and clear
    add_argument ("topic", "Topic", "The specific topic to address", true);
       
    // Avoid: Vague arguments
    // add_argument ("info", "Information", "Some information", true);  // Too generic
    
  2. Use descriptive names:

    // Good: Clear purpose
    add_argument ("target_audience", "Target Audience", "Who the content is for", false);
       
    // Avoid: Generic names
    // add_argument ("param1", "Parameter 1", "First parameter", true);
    
  3. Provide reasonable defaults:

    add_argument ("style", "Style", "Writing style", false, "neutral");
    add_argument ("length", "Length", "Content length", false, "medium");
    

Error Handling

Error Types

The library provides these error types:

try {
    // Your code
} catch (Mcp.Core.Error.INVALID_PARAMS e) {
    // Invalid method parameters
} catch (Mcp.Core.Error.METHOD_NOT_FOUND e) {
    // Method doesn't exist
} catch (Mcp.Core.Error.NOT_FOUND e) {
    // Resource doesn't exist
} catch (Mcp.Core.Error.INTERNAL_ERROR e) {
    // Internal server error
} catch (Mcp.Core.Error.INVALID_ARGUMENT e) {
    // Invalid argument
} catch (Mcp.Core.Error.PARSE_ERROR e) {
    // JSON parsing error
}

Best Practices for Error Handling

  1. Be specific in error messages:

    // Good
    throw new Mcp.Core.Error.INVALID_PARAMS (
       "Temperature must be between -273.15 and 1000 Kelvin"
    );
       
    // Bad
    throw new Mcp.Core.Error.INVALID_PARAMS ("Invalid temperature");
    
  2. Include context in errors:

    throw new Mcp.Core.Error.NOT_FOUND (
       "File not found: %s (working directory: %s)".printf (path, working_dir)
    );
    
  3. Use appropriate error codes:

    // Client error (bad request)
    throw new Mcp.Core.Error.INVALID_PARAMS ("...");
       
    // Server error (something went wrong)
    throw new Mcp.Core.Error.INTERNAL_ERROR ("...");
    

Testing and Debugging

Unit Testing

Test your components individually:

void test_tool_execution () {
    var tool = new MyTool ();
    var arguments = Mcp.Types.VariantUtils.new_dict_builder ();
    arguments.add ("{sv}", "text", new Variant.string ("test"));
    var args = arguments.end ();
    
    // Test synchronous execution
    tool.execute.begin (arguments, (obj, res) => {
        assert (res.is_error == false);
        var content = res.content[0] as Mcp.Types.Common.TextContent;
        assert (content.text.contains ("Processed:"));
    });
}

Integration Testing

Test the full server:

// Test with mock client
void test_server_integration () {
    var server = new MyServer ();
    server.start.begin ();
    
    // Send JSON-RPC messages
    var request = create_json_rpc_request ("tools/list", {});
    var response = server.handle_request (request);
    
    // Verify response
    assert (response != null);
}

Debugging Tips

  1. Enable logging:

    var capabilities = new Mcp.Types.Protocol.ServerCapabilities ();
    capabilities.logging = true;  // Always enable for debugging
    
  2. Use print statements:

    public override async Mcp.Tools.Types.CallToolResult do_execute (Variant arguments) throws Error {
       print ("Executing tool with arguments\n");
           
       var result = yield perform_operation (arguments);
           
       print ("Tool execution completed: %s\n", result.is_error ? "ERROR" : "SUCCESS");
       return result;
    }
    
  3. Test with real MCP clients:

    • Use Claude Desktop or other MCP-compatible clients
    • Test all your tools, resources, and prompts
    • Verify JSON-RPC message flow

Extending the Library

Jsonrpc Integration

The library now uses Jsonrpc.Server from jsonrpc-glib-1.0 for JSON-RPC communication. Custom transport implementations are typically not needed as the standard implementation handles:

  • STDIO communication
  • JSON-RPC 2.0 protocol compliance
  • Error handling and response formatting
  • Async method dispatch

Custom Managers

Create specialized managers for your domain:

Custom Managers

Create specialized managers for your domain:

public class DatabaseResourceManager : Mcp.Resources.Manager {
    private DatabaseConnection db;
    
    public DatabaseResourceManager (DatabaseConnection db) {
        this.db = db;
    }
    
    public override async Gee.ArrayList<Mcp.Resources.Types.Resource> list_resources (string? cursor) throws Error {
        var resources = new Gee.ArrayList<Mcp.Resources.Types.Resource> ();
        
        // Query database
        var results = db.query_resources (cursor);
        foreach (var row in results) {
            var resource = new Mcp.Resources.Types.Resource (row["uri"], row["name"]);
            resources.add (resource);
        }
        
        return resources;
    }
}

Troubleshooting

Common Issues

Server Won't Start

Problem: Server fails to start Causes:

  • Missing dependencies
  • Port already in use
  • Invalid server configuration

Solutions:

# Check dependencies
valac --pkg mcp-vala --pkg json-glib-1.0 --pkg jsonrpc-glib-1.0 --pkg gio-unix-2.0 --pkg gee-0.8 your-server.vala

# Check with strace
strace -e trace=network your-server

# Use debug build
meson configure -Ddebug=true

Resources Not Appearing

Problem: Registered resources don't show up in client Causes:

  • Provider not registered correctly
  • list_resources() method throwing errors
  • URI scheme issues

Solutions:

// Check registration
try {
    server.resource_manager.register_provider ("my_provider", provider);
    print ("Provider registered successfully\n");
} catch (Error e) {
    stderr.printf ("Failed to register provider: %s\n", e.message);
}

// Check URIs
var resource = new Mcp.Resources.Types.Resource ("file:///valid/path", "Name");
// NOT: "invalid-uri" (missing scheme)
// NOT: "file://invalid path" (missing encoding)

Tool Execution Fails

Problem: Tools return errors or don't execute Causes:

  • Invalid JSON schemas
  • Missing required arguments
  • Async method not yielding

Solutions:

// Validate schema
public Mcp.Tools.Types.ToolDefinition get_definition () throws Error {
    if (definition.input_schema == null) {
        throw new Mcp.Core.Error.INTERNAL_ERROR ("Input schema not initialized");
    }
    return definition;
}

// Check async execution
protected override async Mcp.Tools.Types.CallToolResult do_execute (Variant arguments) throws Error {
    // Always yield, even for sync operations
    var result = process_sync (arguments);
    return result;  // Missing yield!
}

Prompt Generation Issues

Problem: Prompts don't generate correctly Causes:

  • Template syntax errors
  • Missing argument substitutions
  • Invalid message structures

Solutions:

// Debug template
protected override Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> generate_messages (Variant arguments) {
    print ("Generating prompt with arguments\n");
    
    var messages = new Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> ();
    
    // Debug template substitution
    string template = get_template ();
    string result = substitute_variables (template, arguments);
    print ("Template result: %s\n", result);
    
    var content = new Mcp.Types.Common.TextContent (result);
    var message = new Mcp.Prompts.Types.PromptMessage ("user", content);
    messages.add (message);
    
    return messages;
}

Performance Issues

Memory Usage

Problem: High memory consumption Solutions:

  • Stream large resources instead of loading entirely
  • Use weak references for cached data
  • Clear caches when not needed

    // Good: Streaming
    public override async Mcp.Types.Common.BlobResourceContents read_resource (string uri) throws Error {
    var stream = yield open_resource_stream (uri);
    var buffer = new uint8[4096];  // Small, fixed buffer
        
    // Process in chunks
    do {
        size_t bytes_read = yield stream.read_async (buffer);
        if (bytes_read == 0) break;
            
        // Process chunk
        yield process_chunk (buffer[0:bytes_read]);
    } while (true);
    }
    
    // Bad: Loading everything
    public override async Mcp.Types.Common.BlobResourceContents read_resource (string uri) throws Error {
    var file = File.new_for_uri (uri);
    uint8[] contents;
    yield file.load_contents_async (null, out contents);
        
    // Loads entire file into memory
    return new Mcp.Types.Common.BlobResourceContents (uri, contents);
    }
    

Concurrency

Problem: Blocking operations affect performance Solutions:

  • Use async methods properly
  • Implement cancellation support
  • Use thread pools for CPU-intensive work

    // Good: Non-blocking
    public override async Mcp.Tools.Types.CallToolResult do_execute (Variant arguments) throws Error {
    // Yield to allow other operations
    var result = yield perform_long_operation (arguments);
    return result;
    }
    
    // Bad: Blocking
    public override async Mcp.Tools.Types.CallToolResult do_execute (Variant arguments) throws Error {
    // This blocks the entire server!
    var result = perform_blocking_operation (arguments);
    return result;
    }
    

Getting Help

  1. Check the examples: All examples demonstrate best practices
  2. Review the source code: The library is fully documented
  3. Enable debug logging: Use the logging capabilities
  4. Test with real clients: Verify with MCP-compatible tools

Conclusion

The MCP Vala library provides a solid foundation for building MCP servers. By following the patterns and best practices in this guide, you can create robust, efficient, and maintainable MCP servers that integrate seamlessly with the Model Context Protocol ecosystem.

The library now uses jsonrpc-glib-1.0 for standard-compliant JSON-RPC 2.0 communication, providing:

  • Better error handling
  • Improved IO reliability
  • Standard protocol compliance
  • Performance optimizations

Remember:

  • Start simple and add complexity incrementally
  • Test each component independently
  • Handle errors gracefully
  • Use the provided base classes
  • Follow Vala naming conventions
  • Document your custom extensions

Happy coding!