Skip to main content

MCP Tool Serialization

The Model Context Protocol (MCP) allows servers to expose tools that can be used by AI models. The Go AI SDK provides methods to retrieve tool definitions in a format that can be stored, transmitted, or cached.

GetSerializableTools()​

The GetSerializableTools() method returns tool definitions from an MCP server in a JSON-serializable format with full pagination support.

Method Signature​

func (c *MCPClient) GetSerializableTools(ctx context.Context) (*ListToolsResult, error)

Return Type​

type ListToolsResult struct {
Tools []MCPTool `json:"tools"`
NextCursor string `json:"nextCursor,omitempty"`
}

type MCPTool struct {
Name string `json:"name"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
InputSchema map[string]interface{} `json:"inputSchema"`
OutputSchema map[string]interface{} `json:"outputSchema,omitempty"`
Annotations map[string]interface{} `json:"annotations,omitempty"`
Meta map[string]interface{} `json:"_meta,omitempty"`
}

MCPTool preserves the current MCP tool JSON shape, including title, outputSchema, annotations, and _meta. MCP Apps metadata is stored in _meta.ui or the legacy flat _meta["ui/resourceUri"] field and can be normalized with mcp.GetMCPAppToolMeta.

Basic Usage​

package main

import (
"context"
"encoding/json"
"fmt"
"log"

"github.com/digitallysavvy/go-ai/pkg/mcp"
)

func main() {
// Create MCP client
transport := mcp.NewStdioTransport(mcp.StdioTransportConfig{
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-everything"},
})

client := mcp.NewMCPClient(transport, mcp.MCPClientConfig{
ClientName: "my-app",
ClientVersion: "1.0.0",
})

// Connect to server
ctx := context.Background()
if err := client.Connect(ctx); err != nil {
log.Fatal(err)
}
defer client.Close()

// Get serializable tools
tools, err := client.GetSerializableTools(ctx)
if err != nil {
log.Fatal(err)
}

// Tools can now be serialized and stored
data, _ := json.MarshalIndent(tools, "", " ")
fmt.Printf("Serialized tools:\n%s\n", string(data))
}

Storing Tool Definitions​

Tool definitions can be cached to avoid repeated server queries:

import (
"encoding/json"
"os"
)

// Cache tools to file
func cacheTools(tools *mcp.ListToolsResult, filename string) error {
data, err := json.Marshal(tools)
if err != nil {
return err
}
return os.WriteFile(filename, data, 0644)
}

// Load tools from cache
func loadCachedTools(filename string) (*mcp.ListToolsResult, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

var tools mcp.ListToolsResult
if err := json.Unmarshal(data, &tools); err != nil {
return nil, err
}

return &tools, nil
}

// Usage
tools, err := client.GetSerializableTools(ctx)
if err != nil {
log.Fatal(err)
}

// Cache for later use
if err := cacheTools(tools, "mcp-tools-cache.json"); err != nil {
log.Printf("Warning: Failed to cache tools: %v", err)
}

Transmitting Tool Definitions​

Tool definitions can be sent over the network:

import (
"bytes"
"encoding/json"
"net/http"
)

// Send tools to remote service
func sendToolsToService(tools *mcp.ListToolsResult, url string) error {
data, err := json.Marshal(tools)
if err != nil {
return err
}

resp, err := http.Post(url, "application/json", bytes.NewReader(data))
if err != nil {
return err
}
defer resp.Body.Close()

return nil
}

// Usage
tools, err := client.GetSerializableTools(ctx)
if err != nil {
log.Fatal(err)
}

err = sendToolsToService(tools, "https://api.example.com/mcp-tools")
if err != nil {
log.Printf("Failed to send tools: %v", err)
}

Pagination Support​

When MCP servers have many tools, results may be paginated:

func getAllTools(client *mcp.MCPClient, ctx context.Context) ([]mcp.MCPTool, error) {
var allTools []mcp.MCPTool

// Get first page
result, err := client.GetSerializableTools(ctx)
if err != nil {
return nil, err
}

allTools = append(allTools, result.Tools...)

// Handle pagination if present.
if result.NextCursor != "" {
log.Printf("More tools available (cursor: %s)", result.NextCursor)
}

return allTools, nil
}

Comparison: ListTools vs GetSerializableTools​

The SDK provides two methods for retrieving tools:

MethodReturn TypeUse Case
ListTools()[]MCPToolConvenience method for immediate use
GetSerializableTools()*ListToolsResultFull result with pagination support for storage/transmission
// Convenience method - just the tools
tools, err := client.ListTools(ctx)
// Returns: []MCPTool

// Serialization method - complete result
result, err := client.GetSerializableTools(ctx)
// Returns: *ListToolsResult with Tools + NextCursor

Use Cases​

1. Tool Registry Service​

Build a centralized registry of available tools:

type ToolRegistry struct {
tools map[string]*mcp.ListToolsResult
}

func (r *ToolRegistry) RegisterServer(serverName string, tools *mcp.ListToolsResult) {
r.tools[serverName] = tools
}

func (r *ToolRegistry) GetAllTools() []*mcp.ListToolsResult {
var all []*mcp.ListToolsResult
for _, tools := range r.tools {
all = append(all, tools)
}
return all
}

2. Offline Tool Discovery​

Cache tools for offline browsing:

func discoverAndCache(servers []string) error {
for _, server := range servers {
client := connectToServer(server)
tools, err := client.GetSerializableTools(ctx)
if err != nil {
log.Printf("Failed to get tools from %s: %v", server, err)
continue
}

filename := fmt.Sprintf("tools-%s.json", server)
if err := cacheTools(tools, filename); err != nil {
log.Printf("Failed to cache tools for %s: %v", server, err)
}
}
return nil
}

3. Tool Versioning​

Track tool changes over time:

type ToolSnapshot struct {
Timestamp time.Time `json:"timestamp"`
Tools *mcp.ListToolsResult `json:"tools"`
}

func snapshotTools(client *mcp.MCPClient) (*ToolSnapshot, error) {
tools, err := client.GetSerializableTools(context.Background())
if err != nil {
return nil, err
}

return &ToolSnapshot{
Timestamp: time.Now(),
Tools: tools,
}, nil
}

Best Practices​

  1. Cache tool definitions: Avoid repeated queries by caching results
  2. Handle pagination: Check for NextCursor and implement pagination when needed
  3. Validate after deserialization: Ensure tools loaded from cache are still valid
  4. Version your cache: Include timestamps or version numbers in cached data
  5. Implement TTL: Refresh cached tools periodically to catch updates

Error Handling​

tools, err := client.GetSerializableTools(ctx)
if err != nil {
var mcpErr *mcp.MCPClientError
switch {
case errors.Is(err, context.DeadlineExceeded):
log.Fatal("Timeout getting tools")
case errors.As(err, &mcpErr):
log.Fatalf("MCP error %d: %s", mcpErr.Code, mcpErr.Message)
default:
log.Fatalf("Failed to get tools: %v", err)
}
}

See Also​