This guide provides comprehensive information for developers who want to use the MCP Vala library to create Model Context Protocol (MCP) servers.
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 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
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"
);
// Create capabilities
var capabilities = new Mcp.Types.Protocol.ServerCapabilities ();
capabilities.logging = true;
// 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 ();
}
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 are functions that clients can execute:
// Tool definition
var input_schema = new Json.Object ();
input_schema.set_string_member ("type", "object");
var definition = new Mcp.Tools.Types.ToolDefinition (
"my_tool",
input_schema
);
// Tool execution result
var result = new Mcp.Tools.Types.CallToolResult ();
var content = new Mcp.Types.Common.TextContent ("Operation completed");
result.content.add (content);
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);
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.McpError.RESOURCE_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 ());
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);
}
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 = new Json.Object ();
input_schema.set_string_member ("type", "object");
var properties = new Json.Object ();
var text_prop = new Json.Object ();
text_prop.set_string_member ("type", "string");
text_prop.set_string_member ("description", "Text to process");
properties.set_object_member ("text", text_prop);
input_schema.set_object_member ("properties", properties);
var required = new Json.Array ();
required.add_string_element ("text");
input_schema.set_array_member ("required", required);
var definition = new Mcp.Tools.Types.ToolDefinition ("my_tool", input_schema);
definition.description = "Processes text input";
base (definition);
}
protected override async Mcp.Tools.Types.CallToolResult do_execute (Json.Object 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 ());
Return structured data with your results:
var structured_data = new Json.Object ();
structured_data.set_string_member ("original", text);
structured_data.set_string_member ("processed", processed);
structured_data.set_int_member ("length", processed.length);
return create_structured_result ("Text processed successfully", structured_data);
Handle errors gracefully:
protected override async Mcp.Tools.Types.CallToolResult do_execute (Json.Object arguments) throws Error {
try {
// Validate input
string text = get_string_arg (arguments, "text");
if (text.length == 0) {
throw new Mcp.Core.McpError.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);
}
}
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 Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> generate_messages (Json.Object arguments) {
var messages = new Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> ();
string topic = get_string_arg (arguments, "topic");
string? style = get_string_arg (arguments, "style", "neutral");
string? length = get_string_arg (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 (Json.Object arguments, string name, string? default_value = null) {
if (arguments.has_member (name)) {
return arguments.get_string_member (name);
}
return default_value;
}
}
// Register with server
server.prompt_manager.register_template ("my_prompt", new MyPrompt ());
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}}""";
Create conversations with multiple messages:
protected override Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> generate_messages (Json.Object 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 {{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 {{topic}}."
);
var user_msg = new Mcp.Prompts.Types.PromptMessage ("user", user_content);
messages.add (user_msg);
return messages;
}
Set up capabilities properly:
var capabilities = new Mcp.Types.Protocol.ServerCapabilities ();
capabilities.logging = true; // Always enable logging
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;
}
Register components before starting:
// Register all providers, executors, and templates
setup_resources ();
setup_tools ();
setup_prompts ();
// Then start the server
yield server.start ();
Use appropriate MIME types:
resource.mime_type = "text/plain"; // Text files
resource.mime_type = "application/json"; // JSON data
resource.mime_type = "image/png"; // Images
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);
}
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 ();
Validate all inputs:
protected override void validate_arguments (Json.Object arguments) throws Error {
// Call base validation
base.validate_arguments (arguments);
// Add custom validation
if (arguments.has_member ("custom_param")) {
string value = arguments.get_string_member ("custom_param");
if (!is_valid_custom_param (value)) {
throw new Mcp.Core.McpError.INVALID_PARAMS ("Invalid custom_param");
}
}
}
Use structured results:
var structured_data = new Json.Object ();
structured_data.set_string_member ("status", "success");
structured_data.set_object_member ("result", result_object);
structured_data.set_double_member ("duration_ms", elapsed_ms);
return create_structured_result ("Operation completed", structured_data);
Handle long-running operations:
protected override async Mcp.Tools.Types.CallToolResult do_execute (Json.Object arguments) throws Error {
// Start progress notification
var progress = new Json.Object ();
progress.set_string_member ("status", "processing");
send_progress_notification (progress);
// Perform long operation
var result = yield perform_long_operation (arguments);
// Final progress
progress.set_string_member ("status", "completed");
send_progress_notification (progress);
return result;
}
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
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);
Provide reasonable defaults:
add_argument ("style", "Style", "Writing style", false, "neutral");
add_argument ("length", "Length", "Content length", false, "medium");
The library provides these error types:
try {
// Your code
} catch (Mcp.Core.McpError.INVALID_PARAMS e) {
// Invalid method parameters
} catch (Mcp.Core.McpError.METHOD_NOT_FOUND e) {
// Method doesn't exist
} catch (Mcp.Core.McpError.RESOURCE_NOT_FOUND e) {
// Resource doesn't exist
} catch (Mcp.Core.McpError.INTERNAL_ERROR e) {
// Internal server error
} catch (Mcp.Core.McpError.TRANSPORT_ERROR e) {
// Communication error
} catch (Mcp.Core.McpError.PARSE_ERROR e) {
// JSON parsing error
}
Be specific in error messages:
// Good
throw new Mcp.Core.McpError.INVALID_PARAMS (
"Temperature must be between -273.15 and 1000 Kelvin"
);
// Bad
throw new Mcp.Core.McpError.INVALID_PARAMS ("Invalid temperature");
Include context in errors:
throw new Mcp.Core.McpError.RESOURCE_NOT_FOUND (
"File not found: %s (working directory: %s)".printf (path, working_dir)
);
Use appropriate error codes:
// Client error (bad request)
throw new Mcp.Core.McpError.INVALID_PARAMS ("...");
// Server error (something went wrong)
throw new Mcp.Core.McpError.INTERNAL_ERROR ("...");
Test your components individually:
void test_tool_execution () {
var tool = new MyTool ();
var arguments = new Json.Object ();
arguments.set_string_member ("text", "test");
// 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:"));
});
}
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);
}
Enable logging:
var capabilities = new Mcp.Types.Protocol.ServerCapabilities ();
capabilities.logging = true; // Always enable for debugging
Use print statements:
public override async Mcp.Tools.Types.CallToolResult do_execute (Json.Object arguments) throws Error {
print ("Executing tool with arguments: %s\n", arguments.to_string (null));
var result = yield perform_operation (arguments);
print ("Tool execution completed: %s\n", result.is_error ? "ERROR" : "SUCCESS");
return result;
}
Test with real MCP clients:
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:
Create specialized managers for your domain:
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;
}
}
Problem: Server fails to start Causes:
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
Problem: Registered resources don't show up in client Causes:
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)
Problem: Tools return errors or don't execute Causes:
Solutions:
// Validate schema
public Mcp.Tools.Types.ToolDefinition get_definition () throws Error {
if (definition.input_schema == null) {
throw new Mcp.Core.McpError.INTERNAL_ERROR ("Input schema not initialized");
}
return definition;
}
// Check async execution
protected override async Mcp.Tools.Types.CallToolResult do_execute (Json.Object arguments) throws Error {
// Always yield, even for sync operations
var result = process_sync (arguments);
return result; // Missing yield!
}
Problem: Prompts don't generate correctly Causes:
Solutions:
// Debug template
protected override Gee.ArrayList<Mcp.Prompts.Types.PromptMessage> generate_messages (Json.Object arguments) {
print ("Generating prompt with arguments: %s\n", arguments.to_string (null));
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;
}
Problem: High memory consumption Solutions:
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);
}
Problem: Blocking operations affect performance Solutions:
Use thread pools for CPU-intensive work
// Good: Non-blocking
public override async Mcp.Tools.Types.CallToolResult do_execute (Json.Object 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 (Json.Object arguments) throws Error {
// This blocks the entire server!
var result = perform_blocking_operation (arguments);
return result;
}
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:
Remember:
Happy coding!