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/mcp// Tool arguments are plain structs with validation tags
// ormc generates Schema(), Pointers(), Validate() automatically
type SearchArgs struct {
Query string `validate:"required,min=1,max=255"`
Limit int64 `validate:"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 := mcp.NewServer(mcp.Config{
Name: "my-server",
Version: "1.0.0",
Auth: myAuth, // implements mcp.Authorizer
}, nil)
srv.AddTool(mcp.Tool{
Name: "search",
Description: "Search items by query",
InputSchema: new(SearchArgs).Schema(),
Resource: "items",
Action: 'r',
Execute: handleSearch,
})
http.Handle("/mcp", srv.HTTPHandler())
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 defines the interface; implementation lives in tinywasm/user:
type Authorizer interface {
Authorize(token string) (userID string, err error)
}
// In your app:
auth := user.NewBearerAuth(secret) // satisfies mcp.AuthorizerGroup related tools and their dependencies in a provider:
type CatalogProvider struct {
db *postgres.DB
}
type CatalogSearchArgs struct {
Query string `validate:"required,min=1,max=255"`
Category string `validate:"max=50"`
}
type CatalogUpdateArgs struct {
ProductID string `validate:"required"`
Price float64 `validate:"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 := 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:
handler := mcp.NewServer(config, providers)
response := handler.HandleMessage(&ctx, message)| Symbol | Description |
|---|---|
NewServer(config, providers) |
Create MCP server |
Server.AddTool(tool) |
Register a single tool |
Server.HTTPHandler() |
HTTP endpoint (server-only) |
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) |
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 |
FilterFunc |
Filter tools by context |
CtxKeySessionID |
Context key for session ID (ctx.Set / ctx.Value) |
See docs/WHY_ARQ.md for architecture decisions and trade-offs.