agents-go

Model context protocol (MCP)

MCP is an open protocol for exposing tools (and other capabilities) to LLM applications. The mcp package connects an agent to MCP servers over the official Go SDK: each server tool becomes a function tool the model can call.

mcp is its own Go module (it carries the go-sdk and its transitive closure, spec §5.7). The import path is unchanged; add it beside the core:

go get github.com/zzir/agents-go/mcp

Connecting a server

import (
	"os/exec"

	"github.com/zzir/agents-go/mcp"
)

// stdio: launch the server as a subprocess
fsServer, err := mcp.NewStdioServer(ctx, "filesystem",
	exec.Command("npx", "-y", "@modelcontextprotocol/server-filesystem", dir),
	mcp.Options{})
if err != nil {  }
defer fsServer.Close()

agent := &agents.Agent{
	Name:       "file assistant",
	MCPServers: []agents.MCPServer{fsServer},
}

Transports:

Constructor Transport
mcp.NewStdioServer(ctx, name, cmd, opts) Subprocess over stdio
mcp.NewStreamableHTTPServer(ctx, name, endpoint, opts) Streamable HTTP
mcp.NewWithTransport(ctx, name, transport, opts) Anything implementing the go-sdk Transport (e.g. in-memory for tests, or the legacy SSE transport)

The agent lists each server’s tools at the start of every turn, so servers may add or remove tools between turns.

Options

mcp.Options{
	AllowedTools: []string{"read_file", "list_directory"}, // expose only these
	BlockedTools: []string{"delete_file"},                 // hide these
	Strict:       true,                                    // normalize schemas to OpenAI strict mode
	ClientName:   "my-app", ClientVersion: "1.2.0",        // reported to the server

	CacheToolsList: true,            // cache list_tools across turns
	ToolNamePrefix: "github_",       // avoid name clashes between servers
	ToolFilter: func(ctx context.Context, rc *agents.RunContext, agent *agents.Agent, name string) bool {
		return rc.Context != nil // expose tools only in some run contexts
	},
	RequireApproval: mcp.ApproveTools("delete_file"), // HITL for these tools
	OAuthHandler: authHandler, // OAuth 2.1 authorization (streamable HTTP only)

	MaxRetryAttempts: 3,              // retry list_tools/call_tool failures (-1 = infinite, 0 = off)
	RetryBackoffBase: time.Second,    // base delay for exponential backoff
}

Two more behaviors are automatic:

OAuth

The mcp package supports OAuth 2.1 for streamable HTTP servers via the go-sdk’s auth package. Set Options.OAuthHandler to an auth.OAuthHandler implementation — the built-in auth.NewAuthorizationCodeHandler covers the standard authorization code + PKCE flow with optional dynamic client registration:

import "github.com/modelcontextprotocol/go-sdk/auth"

handler, _ := auth.NewAuthorizationCodeHandler(&auth.AuthorizationCodeHandlerConfig{
	RedirectURL:              "http://localhost:3142",
	AuthorizationCodeFetcher: myFetcher, // opens browser, waits for redirect
})

srv, _ := mcp.NewStreamableHTTPServer(ctx, "my-server", endpoint, mcp.Options{
	OAuthHandler: handler,
})

The agents-server web UI handles this automatically: configure a server with Authentication → OAuth, and the Connect button will open an authorization popup when needed.

Prompts and resources

Protocol surface this package does not adapt — prompts, resources, and whatever the go-sdk grows next — is reached through the underlying client session (they are not agent tools — call them yourself, e.g. to seed instructions from a server-managed prompt):

prompts, _ := server.Session().ListPrompts(ctx, nil)
p, _ := server.Session().GetPrompt(ctx, &mcpsdk.GetPromptParams{Name: "code_review", Arguments: map[string]string{"lang": "go"}})
// p.Messages -> turn into agent instructions or input

resources, _ := server.Session().ListResources(ctx, nil)
r, _ := server.Session().ReadResource(ctx, &mcpsdk.ReadResourceParams{URI: "file:///README.md"})
// r.Contents -> inject as context

Behavior

Not modeled: provider-hosted MCP (OpenAI’s server-side MCP tool), per the SDK’s no-hosted-tools stance.

Serving your tools over MCP

The same package works in the other direction: mcp.Server connects an agent to somebody else’s tools, and these hand yours to somebody else — an editor, a desktop client, another agent.

srv, err := mcp.NewToolServer(agent.Tools, mcp.ServeOptions{Name: "my-tools"})
if err != nil { log.Fatal(err) }
log.Fatal(mcp.ServeStdio(context.Background(), srv))

The tools are the same values an Agent runs, so a capability written once is available in both places rather than reimplemented for each. Their schemas travel with them, so a client can validate before calling.

To expose a whole agent instead, as one question-shaped tool:

srv, _ := mcp.NewAgentServer(agent, runOpts, mcp.ServeOptions{})

It takes a string and returns the agent’s final output, which is what a caller asking a question wants — it is not driving a turn loop. The agent’s own tools stay inside: they are how it answers, not what it offers.

A tool or run failure comes back as a result, not a protocol error. The caller is a model, and it can act on “that path does not exist” while a transport error only tells it the connection is fine.

Agent names are sanitized into tool names (Research Botask_research_bot): clients key on the name, and one with spaces is one some of them will not call.