Skip to content

Notes Server

A stateful MCP server that stores text notes in memory. This example demonstrates the most important pattern for building stateful servers in mcp.zig: passing a context struct via user_data to share mutable state across all tool handlers.

Source: examples/notes_server.zig

Run

bash
zig build run-notes
# or
./zig-out/bin/notes-server

Features

FeatureDescription
create_note toolCreate or overwrite a note by title
read_note toolRead a note's body by title
delete_note toolDelete a note by title
list_notes toolList all note titles
notes://index resourceDynamic resource listing all titles
Resource templatenotes://{title} for individual notes
NotificationsnotifyResourcesChanged on create/delete

Key Implementation Pattern: user_data Context

zig
// Define a context struct to hold shared mutable state
const Ctx = struct {
    store: NoteStore,
    server: *mcp.Server,
    io: std.Io,
    alloc: std.mem.Allocator,
};

// Initialize context before the server
var ctx: Ctx = .{
    .store = NoteStore.init(allocator),
    .server = &server,
    .io = io,
    .alloc = allocator,
};

// Pass &ctx as user_data when registering tools
try server.addTool(.{
    .name = "create_note",
    .user_data = &ctx,
    .handler = createNoteHandler,
    // ...
});

Then in each handler, cast user_data back:

zig
fn createNoteHandler(
    user_data: ?*anyopaque,
    io: std.Io,
    allocator: std.mem.Allocator,
    args: ?std.json.Value,
) mcp.tools.ToolError!mcp.tools.ToolResult {
    const ctx: *Ctx = @ptrCast(@alignCast(user_data.?));
    // ... use ctx.store ...
}

List-Change Notifications

When a note is created or deleted, the server notifies connected clients that the resource list has changed:

zig
ctx.server.notifyResourcesChanged(io, allocator) catch {};

This triggers notifications/resources/list_changed on all subscribed clients, causing them to refresh their resource list.

Tools Schema

All tools use InputSchemaBuilder:

zig
fn buildCreateSchema(allocator: std.mem.Allocator) !mcp.types.InputSchema {
    var b = mcp.schema.InputSchemaBuilder.init(allocator);
    defer b.deinit(allocator);
    _ = b.setSchemaDialect("https://json-schema.org/draft/2020-12/schema");
    _ = try b.addString(allocator, "title", "Unique note title", true);
    _ = try b.addString(allocator, "body", "Note content (plain text)", true);
    return b.toInputSchema(allocator);
}

Claude Desktop Configuration

json
{
  "mcpServers": {
    "notes": {
      "command": "/path/to/zig-out/bin/notes-server"
    }
  }
}

Notes are in-memory and will be reset on restart.