Skip to content

tinywasm/mcp

Repository files navigation

tinywasm/mcp

Lean Go implementation of the Model Context Protocol (MCP) over JSON-RPC 2.0. Protocol-only, WASM-safe, minimal public API.

Installation

go get github.com/tinywasm/mcp

ormc (code generation)

Tools use ormc for automatic Schema(), Pointers(), and Validate() generation:

go install github.com/tinywasm/orm/cmd/ormc@latest

Quickstart

1. Define tool arguments with validation (using ormc)

// 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"`
}

2. Write the handler

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
}

3. Register and serve

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)

4. Tool results

return mcp.Text("operation completed"), nil   // text
return mcp.JSON(&MyData{Name: "test"})        // JSON (Fielder)
text, err := mcp.GetText(result)              // extract text

Authorizer

mcp 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.Authorizer

Auth is required — NewServer returns error if Config.Auth == nil. Use mcp.OpenAuthorizer() for open access.


SSE (Streamable HTTP)

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.


ToolProvider

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}})

WASM / Browser

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)

API Reference

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.

About

implementation of the Model Context Protocol (MCP)

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages