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
- Client → Server:
initializerequest with capabilities - Server → Client: response with server capabilities
- Client → Server:
initializednotification - …normal operation…
- Client → Server:
shutdownrequest (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, ¶ms)
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
| Method | Direction | Description |
|---|---|---|
initialize | Client → Server | Start session, exchange capabilities |
initialized | Client → Server | Confirm initialization (notification) |
tools/list | Client → Server | Get available tools |
tools/call | Client → Server | Execute a tool |
resources/list | Client → Server | Get available resources |
resources/read | Client → Server | Read a resource |
prompts/list | Client → Server | Get available prompts |
prompts/get | Client → Server | Get a specific prompt |
Error Codes
| Code | Meaning |
|---|---|
| -32700 | Parse error |
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal 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