Lean Go implementation of the Model Context Protocol (MCP) over JSON-RPC 2.0. Protocol-only, WASM-safe, minimal public API.
go get github.com/tinywasm/mcpTools use ormc for automatic Schema(), Pointers(), and Validate() generation:
go install github.com/tinywasm/orm/cmd/ormc@latest// Tool arguments are plain structs with validation tags
// ormc generates Schema(), Pointers(), Validate() automatically
type SearchArgs struct {
Query string `input:"required,min=1,max=255"`
Limit int64 `input:"min=1,max=100"`
}func handleSearch(ctx *context.Context, req mcp.Request) (*mcp.Result, error) {
var args SearchArgs
if err := req.Bind(&args); err != nil {
return nil, err // server wraps as JSON-RPC error
}
return mcp.Text("found 3 results"), nil
}srv, err := mcp.NewServer(mcp.Config{
Name: "my-server",
Version: "1.0.0",
Auth: mcp.NewTokenAuthorizer("my-secret-key"),
}, nil)
if err != nil {
log.Fatal(err)
}
srv.AddTool(mcp.Tool{
Name: "search",
Description: "Search items by query",
InputSchema: new(SearchArgs).Schema(),
Resource: "items",
Action: 'r',
Execute: handleSearch,
})
// Consumer owns HTTP routing — use HandleMessage directly
http.HandleFunc("/mcp", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
ctx := context.New()
ctx.Set(mcp.CtxKeyAuthToken, r.Header.Get("Authorization"))
resp := srv.HandleMessage(&ctx, body)
// encode resp as JSON and write to w
})
http.ListenAndServe(":3030", nil)return mcp.Text("operation completed"), nil // text
return mcp.JSON(&MyData{Name: "test"}) // JSON (Fielder)
text, err := mcp.GetText(result) // extract textmcp provides built-in implementations and accepts custom ones:
type Authorizer interface {
Authorize(token string) (userID string, err error)
Can(userID, resource string, action byte) bool
}
// Built-in:
auth := mcp.NewTokenAuthorizer("my-api-key") // token-based, Can() always true
auth := mcp.OpenAuthorizer() // no auth, Can() always true
// Custom (e.g. from tinywasm/user):
auth := userModule.MCPAuthorizer() // satisfies mcp.AuthorizerAuth is required — NewServer returns error if Config.Auth == nil. Use mcp.OpenAuthorizer() for open access.
Optional streaming via SSEPublisher interface. The consumer creates the SSE server and injects it:
import "github.com/tinywasm/sse"
sseServer := sse.New()
srv, err := mcp.NewServer(mcp.Config{
Name: "my-server",
Version: "1.0.0",
Auth: mcp.NewTokenAuthorizer("secret"),
SSE: sseServer, // *sse.SSEServer satisfies mcp.SSEPublisher
}, providers)When SSE is present, tool list changes and notifications are published automatically. When nil, no streaming occurs.
Group related tools and their dependencies in a provider:
type CatalogProvider struct {
db *postgres.DB
}
type CatalogSearchArgs struct {
Query string `input:"required,min=1,max=255"`
Category string `input:"max=50"`
}
type CatalogUpdateArgs struct {
ProductID string `input:"required"`
Price float64 `input:"required,min=0"`
}
func (p *CatalogProvider) Tools() []mcp.Tool {
return []mcp.Tool{
{
Name: "catalog_search",
Description: "Search product catalog",
InputSchema: new(CatalogSearchArgs).Schema(),
Resource: "catalog",
Action: 'r',
Execute: p.handleSearch,
},
{
Name: "catalog_update",
Description: "Update product price",
InputSchema: new(CatalogUpdateArgs).Schema(),
Resource: "catalog",
Action: 'u',
Execute: p.handleUpdate,
},
}
}
func (p *CatalogProvider) handleSearch(ctx *context.Context, req mcp.Request) (*mcp.Result, error) {
var args CatalogSearchArgs
if err := req.Bind(&args); err != nil {
return nil, err
}
// ... query p.db
return mcp.Text("found 3 products"), nil
}
// Pass providers to NewServer
srv, err := mcp.NewServer(config, []mcp.ToolProvider{&CatalogProvider{db: db}})The protocol core compiles with TinyGo. Server-only files (//go:build !wasm)
are excluded automatically.
In browser mode, call the handler directly — no HTTP server needed:
srv, err := mcp.NewServer(config, providers)
response := srv.HandleMessage(&ctx, message)| Symbol | Description |
|---|---|
NewServer(config, providers) |
Create MCP server — returns (*Server, error) |
Server.AddTool(tool) |
Register a single tool |
Server.HandleMessage(ctx, msg) |
Process JSON-RPC message (WASM-safe) |
Tool{Name, Description, InputSchema, Resource, Action, Execute} |
Tool definition |
ToolProvider |
Interface: Tools() []Tool |
Authorizer |
Interface: Authorize(token) (userID, error), Can(userID, resource, action) bool |
SSEPublisher |
Interface: Publish(data, channel) (build !wasm) |
NewTokenAuthorizer(apiKey) |
Token-based authorizer |
OpenAuthorizer() |
Open access authorizer |
Request |
Incoming tool call |
Request.Bind(target) |
Decode + validate arguments |
Result |
Tool call result |
Text(s) |
Create text result |
JSON(data) |
Create JSON result |
GetText(result) |
Extract text from result |
Config |
Server configuration: Name, Version, Auth, SSE |
CtxKeySessionID |
Context key for session ID |
CtxKeyUserID |
Context key for authenticated user ID |
CtxKeyAuthToken |
Context key for auth token |
See docs/WHY_ARQ.md for architecture decisions and trade-offs.