This guide shows how to implement an MCP server in Go with a practical focus on three core surfaces exposed to clients: tools, resources, and prompts. It uses the github.com/viant/mcp server runtime together with protocol helpers from github.com/viant/mcp-protocol.
Create a server using the default handler registry. This lets you register tools, resources, and prompts without writing a bespoke handler type. Example snippets below use a small helper to take addresses:
// helper used in examples
func ptr[T any](v T) *T { return &v }package main
import (
"context"
"log"
proto "github.com/viant/mcp-protocol/server"
"github.com/viant/mcp-protocol/schema"
"github.com/viant/mcp/server"
)
func main() {
newHandler := proto.WithDefaultHandler(context.Background(), func(h *proto.DefaultHandler) error {
// Registration happens here (tools/resources/prompts). See sections below.
return nil
})
srv, err := server.New(
server.WithNewHandler(newHandler),
server.WithImplementation(schema.Implementation{Name: "example", Version: "1.0"}),
)
if err != nil {
log.Fatal(err)
}
log.Fatal(srv.HTTP(context.Background(), ":4981").ListenAndServe())
}On initialize, the default handler automatically advertises server capabilities based on what you register:
- Register at least one tool → capabilities.tools set
- Register at least one resource → capabilities.resources set
- Register at least one prompt → capabilities.prompts set
Tools are typed functions the client can call. Use RegisterTool to derive JSON Schemas automatically from your input/output structs.
type AddInput struct {
A int `json:"a"` // required (no pointer, no omitempty)
B int `json:"b"` // required
Note *string `json:"note,omitempty"` // optional pointer
}
type AddOutput struct {
Sum int `json:"sum"`
}
// Inside the WithDefaultHandler block:
if err := proto.RegisterTool[*AddInput, *AddOutput](
h.Registry,
"add", // tool name
"Add two numbers", // description
func(ctx context.Context, in *AddInput) (*schema.CallToolResult, *jsonrpc.Error) {
out := &AddOutput{Sum: in.A + in.B}
// Return either text content or structuredContent per MCP.
// Here we return a JSON-encoded text payload for simplicity.
data, _ := json.Marshal(out)
return &schema.CallToolResult{
Content: []schema.CallToolResultContentElem{
{Text: string(data)}, // schema.TextContent via alias
},
}, nil
},
); err != nil {
return err
}Schema derivation notes:
- Required vs optional is inferred from struct shape and tags:
- Non-pointer, no
omitempty→ required - Pointer or
omitempty→ optional required:"true"forces required;required:"false"oroptionalmarks optional
- Non-pointer, no
- Additional tags supported by the schema helpers:
description,format,choice:"val"
Compatibility note: For clients with protocol version older than 2025-03-26, the default handler omits outputSchema from tools/list automatically.
Client tip: Use schema.NewCallToolRequestParams(name, inputStruct) to build request params from a typed input.
Resources expose readable content by URI and can be subscribed to for change notifications.
// A simple text resource at /hello
h.RegisterResource(schema.Resource{
Name: "hello",
Uri: "/hello",
// MimeType: ptr("text/plain"), // optional
}, func(ctx context.Context, req *schema.ReadResourceRequest) (*schema.ReadResourceResult, *jsonrpc.Error) {
return &schema.ReadResourceResult{
Contents: []schema.ReadResourceResultContentsElem{{
Text: "Hello, world!", // schema.TextResourceContents via alias
Uri: req.Params.Uri,
}},
}, nil
})The default handler implements:
resources/listusing what you registerresources/readby dispatching to your handlerresources/subscribeandresources/unsubscribewith a built-in subscription map
If the underlying content changes, notify subscribed clients. Use the handler notifier to emit resources/updated.
// When a file changes, etc.:
notification, _ := jsonrpc.NewNotification(
schema.MethodNotificationResourceUpdated,
map[string]string{"uri": "/hello"},
)
_ = h.Notifier.Send(context.Background(), notification)You can advertise URI templates via RegisterResourceTemplate and resources/templates/list. This is useful to signal supported URI shapes (e.g., file://{path}). The default ReadResource uses exact URI dispatch; implement a custom ReadResource if you need pattern matching.
h.RegisterResourceTemplate(schema.ResourceTemplate{
Name: "local file",
UriTemplate: "file://{path}",
Description: ptr("Read local files"),
}, func(ctx context.Context, req *schema.ReadResourceRequest) (*schema.ReadResourceResult, *jsonrpc.Error) {
// Implement reading from req.Params.Uri
// ...
return &schema.ReadResourceResult{ /* ... */ }, nil
})Prompts are server-provided prompt templates. Clients list them and request a fully rendered prompt via prompts/get with arguments.
welcome := &schema.Prompt{
Name: "welcome",
Description: ptr("Greets a user by name"),
Arguments: []schema.PromptArgument{
{Name: "name", Title: ptr("User Name"), Required: ptr(true)},
},
}
h.RegisterPrompts(welcome, func(ctx context.Context, p *schema.GetPromptRequestParams) (*schema.GetPromptResult, *jsonrpc.Error) {
name := p.Arguments["name"]
return &schema.GetPromptResult{
Description: ptr("Simple welcome prompt"),
Messages: []schema.PromptMessage{
{
Role: schema.RoleUser,
Content: schema.TextContent{Type: "text", Text: "Please greet the user warmly."},
},
{
Role: schema.RoleAssistant,
Content: schema.TextContent{Type: "text", Text: "Hello, " + name + "!"},
},
},
}, nil
})The default handler provides:
prompts/listmapped to registered promptsprompts/getwith required-argument validation based on yourPrompt.Argumentdefinitions
Supported PromptMessage content includes text, images, audio, and resource embeddings or links.
Combining everything in one handler:
newHandler := proto.WithDefaultHandler(context.Background(), func(h *proto.DefaultHandler) error {
// 1) Resource
h.RegisterResource(schema.Resource{Name: "hello", Uri: "/hello"}, func(ctx context.Context, req *schema.ReadResourceRequest) (*schema.ReadResourceResult, *jsonrpc.Error) {
return &schema.ReadResourceResult{Contents: []schema.ReadResourceResultContentsElem{{Text: "Hello!", Uri: req.Params.Uri}}}, nil
})
// 2) Tool
type EchoIn struct{ Msg string `json:"msg"` }
type EchoOut struct{ Msg string `json:"msg"` }
_ = proto.RegisterTool[*EchoIn, *EchoOut](h.Registry, "echo", "Echo a message", func(ctx context.Context, in *EchoIn) (*schema.CallToolResult, *jsonrpc.Error) {
data, _ := json.Marshal(&EchoOut{Msg: in.Msg})
return &schema.CallToolResult{Content: []schema.CallToolResultContentElem{{Text: string(data)}}}, nil
})
// 3) Prompt
prompt := &schema.Prompt{Name: "welcome", Arguments: []schema.PromptArgument{{Name: "name", Required: ptr(true)}}}
h.RegisterPrompts(prompt, func(ctx context.Context, p *schema.GetPromptRequestParams) (*schema.GetPromptResult, *jsonrpc.Error) {
return &schema.GetPromptResult{Messages: []schema.PromptMessage{{Role: schema.RoleAssistant, Content: schema.TextContent{Type: "text", Text: "Hello, " + p.Arguments["name"] + "!"}}}}, nil
})
return nil
})- Use
server.WithCORS,server.WithProtocolVersion, and HTTP auth middleware fromserver/authwhen wiringserver.New. - For long-running changes (file watches, etc.), use
h.Notifier.Send(...)to emitresources/updated. - To test without a client, the
server.Adaptercan call your handler directly with typed requests.
References:
- Core runtime:
github.com/viant/mcp/server - Protocol + registries:
github.com/viant/mcp-protocol/serverandgithub.com/viant/mcp-protocol/schema - Examples: see the
exampledirectory in this repo