How to Build an MCP Server from Scratch

Guide to creating a Model Context Protocol (MCP) server. Example: Google Calendar integration in Go.

What is MCP?

Model Context Protocol — an open protocol for integrating LLM applications with external data sources and tools.

Key concepts:

  • Tools — functions that LLM can call (model-controlled)
  • Resources — data/context for the model (application-controlled)
  • Prompts — templates for interaction (user-controlled)

Protocol Basics

Transport

MCP supports two transports:

  • stdio — communication via stdin/stdout (simplest, for local servers)
  • HTTP/SSE — for remote servers

Message Format

All messages are JSON-RPC 2.0:

// Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": { "name": "get_events", "arguments": {} }
}
 
// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": { "content": [{ "type": "text", "text": "..." }] }
}
 
// Notification (no response expected)
{
  "jsonrpc": "2.0",
  "method": "notifications/tools/list_changed"
}

Lifecycle

  1. Client → Server: initialize request with capabilities
  2. Server → Client: response with server capabilities
  3. Client → Server: initialized notification
  4. …normal operation…
  5. Client → Server: shutdown request (optional)

Tool Definition

A tool is a function with JSON Schema for input parameters:

{
  "name": "create_event",
  "description": "Create a calendar event",
  "inputSchema": {
    "type": "object",
    "properties": {
      "summary": {
        "type": "string",
        "description": "Event title"
      },
      "start": {
        "type": "string",
        "description": "Start time in RFC3339 format"
      },
      "end": {
        "type": "string",
        "description": "End time in RFC3339 format"
      }
    },
    "required": ["summary", "start", "end"]
  }
}

Tool Call Flow

Client                          Server
  |                               |
  |-- tools/list ---------------->|
  |<-- list of tools -------------|
  |                               |
  |-- tools/call {name, args} --->|
  |<-- result {content} ----------|

Example: Google Calendar MCP Server (Go)

Project Structure

calendar-mcp/
├── main.go
├── calendar.go
├── go.mod
└── go.sum

go.mod

module calendar-mcp
 
go 1.22
 
require (
    github.com/modelcontextprotocol/go-sdk v0.1.0
    google.golang.org/api v0.200.0
)

main.go

package main
 
import (
    "context"
    "log"
    "os"
 
    "github.com/modelcontextprotocol/go-sdk/mcp"
)
 
func main() {
    // Load config from environment
    credentialsFile := os.Getenv("GOOGLE_CREDENTIALS_FILE")
    calendarID := os.Getenv("CALENDAR_ID")
    
    if credentialsFile == "" || calendarID == "" {
        log.Fatal("GOOGLE_CREDENTIALS_FILE and CALENDAR_ID must be set")
    }
 
    // Initialize calendar client
    cal, err := NewCalendarClient(credentialsFile, calendarID)
    if err != nil {
        log.Fatal(err)
    }
 
    // Create MCP server
    server := mcp.NewServer(&mcp.Implementation{
        Name:    "google-calendar",
        Version: "1.0.0",
    }, nil)
 
    // Register tools
    registerTools(server, cal)
 
    // Run server on stdio
    if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
        log.Fatal(err)
    }
}
 
func registerTools(server *mcp.Server, cal *CalendarClient) {
    // Tool: list_events
    mcp.AddTool(server, &mcp.Tool{
        Name:        "list_events",
        Description: "List upcoming calendar events",
    }, func(ctx context.Context, req *mcp.CallToolRequest, input ListEventsInput) (*mcp.CallToolResult, ListEventsOutput, error) {
        events, err := cal.ListEvents(ctx, input.MaxResults, input.TimeMin, input.TimeMax)
        if err != nil {
            return nil, ListEventsOutput{}, err
        }
        return nil, ListEventsOutput{Events: events}, nil
    })
 
    // Tool: create_event
    mcp.AddTool(server, &mcp.Tool{
        Name:        "create_event",
        Description: "Create a new calendar event",
    }, func(ctx context.Context, req *mcp.CallToolRequest, input CreateEventInput) (*mcp.CallToolResult, CreateEventOutput, error) {
        event, err := cal.CreateEvent(ctx, input.Summary, input.Description, input.Start, input.End)
        if err != nil {
            return nil, CreateEventOutput{}, err
        }
        return nil, CreateEventOutput{EventID: event.Id, Link: event.HtmlLink}, nil
    })
 
    // Tool: delete_event
    mcp.AddTool(server, &mcp.Tool{
        Name:        "delete_event",
        Description: "Delete a calendar event by ID",
    }, func(ctx context.Context, req *mcp.CallToolRequest, input DeleteEventInput) (*mcp.CallToolResult, DeleteEventOutput, error) {
        err := cal.DeleteEvent(ctx, input.EventID)
        if err != nil {
            return nil, DeleteEventOutput{Success: false}, err
        }
        return nil, DeleteEventOutput{Success: true}, nil
    })
}
 
// Input/Output structs with JSON schema tags
 
type ListEventsInput struct {
    MaxResults int    `json:"max_results,omitempty" jsonschema:"description=Maximum number of events to return,default=10"`
    TimeMin    string `json:"time_min,omitempty" jsonschema:"description=Start of time range (RFC3339)"`
    TimeMax    string `json:"time_max,omitempty" jsonschema:"description=End of time range (RFC3339)"`
}
 
type ListEventsOutput struct {
    Events []CalendarEvent `json:"events"`
}
 
type CreateEventInput struct {
    Summary     string `json:"summary" jsonschema:"description=Event title,required"`
    Description string `json:"description,omitempty" jsonschema:"description=Event description"`
    Start       string `json:"start" jsonschema:"description=Start time (RFC3339),required"`
    End         string `json:"end" jsonschema:"description=End time (RFC3339),required"`
}
 
type CreateEventOutput struct {
    EventID string `json:"event_id"`
    Link    string `json:"link"`
}
 
type DeleteEventInput struct {
    EventID string `json:"event_id" jsonschema:"description=ID of event to delete,required"`
}
 
type DeleteEventOutput struct {
    Success bool `json:"success"`
}
 
type CalendarEvent struct {
    ID      string `json:"id"`
    Summary string `json:"summary"`
    Start   string `json:"start"`
    End     string `json:"end"`
}

calendar.go

package main
 
import (
    "context"
    "time"
 
    "google.golang.org/api/calendar/v3"
    "google.golang.org/api/option"
)
 
type CalendarClient struct {
    service    *calendar.Service
    calendarID string
}
 
func NewCalendarClient(credentialsFile, calendarID string) (*CalendarClient, error) {
    ctx := context.Background()
    
    srv, err := calendar.NewService(ctx,
        option.WithCredentialsFile(credentialsFile),
        option.WithScopes(calendar.CalendarScope),
    )
    if err != nil {
        return nil, err
    }
    
    return &CalendarClient{
        service:    srv,
        calendarID: calendarID,
    }, nil
}
 
func (c *CalendarClient) ListEvents(ctx context.Context, maxResults int, timeMin, timeMax string) ([]CalendarEvent, error) {
    if maxResults == 0 {
        maxResults = 10
    }
    if timeMin == "" {
        timeMin = time.Now().Format(time.RFC3339)
    }
    
    call := c.service.Events.List(c.calendarID).
        SingleEvents(true).
        OrderBy("startTime").
        MaxResults(int64(maxResults)).
        TimeMin(timeMin)
    
    if timeMax != "" {
        call = call.TimeMax(timeMax)
    }
    
    events, err := call.Context(ctx).Do()
    if err != nil {
        return nil, err
    }
    
    result := make([]CalendarEvent, 0, len(events.Items))
    for _, e := range events.Items {
        start := e.Start.DateTime
        if start == "" {
            start = e.Start.Date
        }
        end := e.End.DateTime
        if end == "" {
            end = e.End.Date
        }
        result = append(result, CalendarEvent{
            ID:      e.Id,
            Summary: e.Summary,
            Start:   start,
            End:     end,
        })
    }
    
    return result, nil
}
 
func (c *CalendarClient) CreateEvent(ctx context.Context, summary, description, start, end string) (*calendar.Event, error) {
    event := &calendar.Event{
        Summary:     summary,
        Description: description,
        Start: &calendar.EventDateTime{
            DateTime: start,
        },
        End: &calendar.EventDateTime{
            DateTime: end,
        },
    }
    
    return c.service.Events.Insert(c.calendarID, event).Context(ctx).Do()
}
 
func (c *CalendarClient) DeleteEvent(ctx context.Context, eventID string) error {
    return c.service.Events.Delete(c.calendarID, eventID).Context(ctx).Do()
}

Configuration for Claude

Add to ~/.claude.json:

{
  "mcpServers": {
    "google-calendar": {
      "command": "/path/to/calendar-mcp",
      "env": {
        "GOOGLE_CREDENTIALS_FILE": "/path/to/service-account.json",
        "CALENDAR_ID": "your-email@gmail.com"
      }
    }
  }
}

Or for Takopi in ~/.config/takopi/takopi.toml, the MCP configuration would be added similarly.

Building Without SDK (Raw JSON-RPC)

If you want to understand the protocol deeply or use a language without SDK:

Minimal Server Structure

package main
 
import (
    "bufio"
    "encoding/json"
    "fmt"
    "os"
)
 
type JSONRPCRequest struct {
    JSONRPC string          `json:"jsonrpc"`
    ID      interface{}     `json:"id,omitempty"`
    Method  string          `json:"method"`
    Params  json.RawMessage `json:"params,omitempty"`
}
 
type JSONRPCResponse struct {
    JSONRPC string      `json:"jsonrpc"`
    ID      interface{} `json:"id,omitempty"`
    Result  interface{} `json:"result,omitempty"`
    Error   *RPCError   `json:"error,omitempty"`
}
 
type RPCError struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}
 
func main() {
    scanner := bufio.NewScanner(os.Stdin)
    encoder := json.NewEncoder(os.Stdout)
 
    for scanner.Scan() {
        var req JSONRPCRequest
        if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
            continue
        }
 
        response := handleRequest(req)
        if response != nil {
            encoder.Encode(response)
        }
    }
}
 
func handleRequest(req JSONRPCRequest) *JSONRPCResponse {
    switch req.Method {
    case "initialize":
        return &JSONRPCResponse{
            JSONRPC: "2.0",
            ID:      req.ID,
            Result: map[string]interface{}{
                "protocolVersion": "2024-11-05",
                "serverInfo": map[string]string{
                    "name":    "my-server",
                    "version": "1.0.0",
                },
                "capabilities": map[string]interface{}{
                    "tools": map[string]bool{},
                },
            },
        }
 
    case "tools/list":
        return &JSONRPCResponse{
            JSONRPC: "2.0",
            ID:      req.ID,
            Result: map[string]interface{}{
                "tools": []map[string]interface{}{
                    {
                        "name":        "hello",
                        "description": "Say hello",
                        "inputSchema": map[string]interface{}{
                            "type":       "object",
                            "properties": map[string]interface{}{},
                        },
                    },
                },
            },
        }
 
    case "tools/call":
        var params struct {
            Name      string          `json:"name"`
            Arguments json.RawMessage `json:"arguments"`
        }
        json.Unmarshal(req.Params, &params)
 
        if params.Name == "hello" {
            return &JSONRPCResponse{
                JSONRPC: "2.0",
                ID:      req.ID,
                Result: map[string]interface{}{
                    "content": []map[string]string{
                        {"type": "text", "text": "Hello, world!"},
                    },
                },
            }
        }
 
    case "initialized":
        // Notification, no response
        return nil
    }
 
    return &JSONRPCResponse{
        JSONRPC: "2.0",
        ID:      req.ID,
        Error: &RPCError{
            Code:    -32601,
            Message: "Method not found",
        },
    }
}

Key Protocol Methods

MethodDirectionDescription
initializeClient → ServerStart session, exchange capabilities
initializedClient → ServerConfirm initialization (notification)
tools/listClient → ServerGet available tools
tools/callClient → ServerExecute a tool
resources/listClient → ServerGet available resources
resources/readClient → ServerRead a resource
prompts/listClient → ServerGet available prompts
prompts/getClient → ServerGet a specific prompt

Error Codes

CodeMeaning
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error

Testing Your Server

# Build
go build -o calendar-mcp
 
# Test manually
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | ./calendar-mcp
 
# Test tools/list
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | ./calendar-mcp

Resources