Skip to content

Model Context Protocol (MCP)

The MCP (Model Context Protocol) plugin connects Genkit to MCP servers and lets you publish your own Genkit tools and resources as an MCP server. Connect to one server with GenkitMCPClient, to several with MCPHost, or expose your own application with NewMCPServer.

The full API reference is the package documentation: pkg.go.dev/github.com/firebase/genkit/go/plugins/mcp.

This plugin requires MCP servers to be available. For testing and development, you can use:

  • mcp-server-time - Simple server exposing time operations
  • @modelcontextprotocol/server-everything - A comprehensive MCP server for testing
  • Custom MCP servers written in Python, TypeScript, or other languages

To connect to a single MCP server, create a GenkitMCPClient. This program starts mcp-server-time as a child process, hands its tools to a model, and shuts the child down on exit:

package main
import (
"context"
"log"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/googlegenai"
"github.com/firebase/genkit/go/plugins/mcp"
)
func main() {
ctx := context.Background()
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))
client, err := mcp.NewGenkitMCPClient(mcp.MCPClientOptions{
Name: "mcp-server-time",
Stdio: &mcp.StdioConfig{
Command: "uvx",
Args: []string{"mcp-server-time"},
},
})
if err != nil {
log.Fatal(err)
}
defer client.Disconnect()
tools, err := client.GetActiveTools(ctx, g)
if err != nil {
log.Fatal(err)
}
// ai.WithTools takes ai.ToolRef, so widen the slice first.
refs := make([]ai.ToolRef, 0, len(tools))
for _, t := range tools {
refs = append(refs, t)
}
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("What time is it in Tokyo?"),
ai.WithTools(refs...),
)
if err != nil {
log.Fatal(err)
}
log.Println(resp.Text())
}

Later examples reuse ctx, g, and client from this program and the same import block, plus fmt, net/http, os, os/signal, strings, syscall, and time from the standard library where the snippet uses them.

To manage connections to multiple MCP servers, use MCPHost:

host, err := mcp.NewMCPHost(g, mcp.MCPHostOptions{
Name: "my-app",
MCPServers: []mcp.MCPServerConfig{
{
Name: "everything-server",
Config: mcp.MCPClientOptions{
Name: "everything-server",
Stdio: &mcp.StdioConfig{
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-everything"},
},
},
},
{
Name: "mcp-server-time",
Config: mcp.MCPClientOptions{
Name: "mcp-server-time",
Stdio: &mcp.StdioConfig{
Command: "uvx",
Args: []string{"mcp-server-time"},
},
},
},
},
})
if err != nil {
log.Fatal(err)
}

GetActiveTools returns every tool the connected server advertises:

tools, err := client.GetActiveTools(ctx, g)
if err != nil {
log.Fatal(err)
}

There is no single-tool getter on the client. To use one tool, select it from the returned slice. host.GetActiveTools(ctx, g) is the same call across every server the host is connected to.

Every action a client registers is namespaced with the client’s Name and an underscore: <clientName>_<toolName>. A client named mcp-server-time exposing get_current_time registers the tool as mcp-server-time_get_current_time. Prompts and resources use the same form. To attribute a tool back to its server, cut the name at the first underscore:

for _, t := range tools {
server, _, _ := strings.Cut(t.Name(), "_")
fmt.Println(t.Name(), "from", server)
}

Unlike a Genkit-native tool, an MCP tool returns the MCP result object unchanged. The Genkit Go plugin performs no text or JSON coercion, so the tool’s output is a *mcp.CallToolResult from github.com/mark3labs/mcp-go/mcp, with its Content array intact. A model consuming the tool sees that object. Your own code, if it runs a tool directly or inspects a tool response part, has to walk the array. mcp.ExtractTextFromContent pulls the text out of one content item:

// mcpgo "github.com/mark3labs/mcp-go/mcp"
result, ok := out.(*mcpgo.CallToolResult) // out is the tool's output value
if !ok {
log.Fatalf("unexpected tool output type %T", out)
}
for _, c := range result.Content {
if text := mcp.ExtractTextFromContent(c); text != "" {
fmt.Println(text)
}
}

A disabled or disconnected client returns nil, nil from GetActiveTools, so an empty list is not distinguishable from a server with no tools. Check client.IsEnabled() when the difference matters.

Resources are content a server offers by URI, which a model can pull in. GetActiveResources returns them as Genkit ai.Resource values, namespaced the same way as tools:

resources, err := client.GetActiveResources(ctx)
if err != nil {
log.Fatal(err)
}
for _, r := range resources {
fmt.Println(r.Name())
}

host.GetActiveResources(ctx) does the same across every connected server. Both static resources and URI templates are returned. Unlike GetActiveTools, these calls return an error when the client is disabled or not connected.

You can define local resources with genkit.DefineResource, and an MCP server you run publishes them (see Running as an MCP server):

genkit.DefineResource(g, "handbook", &ai.ResourceOptions{
URI: "file:///docs/handbook.md",
Description: "Company handbook",
}, func(ctx context.Context, input *ai.ResourceInput) (*ai.ResourceOutput, error) {
b, err := os.ReadFile("/docs/handbook.md")
if err != nil {
return nil, err
}
return &ai.ResourceOutput{Content: []*ai.Part{ai.NewTextPart(string(b))}}, nil
})

GetPrompt fetches a prompt from a connected server, registers it on the Genkit instance under its namespaced name, and returns it as an ai.Prompt:

func (c *GenkitMCPClient) GetPrompt(ctx context.Context, g *genkit.Genkit, promptName string, args map[string]string) (ai.Prompt, error)
func (h *MCPHost) GetPrompt(ctx context.Context, g *genkit.Genkit, serverName, promptName string, args map[string]string) (ai.Prompt, error)

MCP prompt arguments are string-valued. Pass nil when the prompt takes none. The return value is an ai.Prompt, not prompt text, so run it with Execute rather than passing it to ai.WithPrompt:

prompt, err := client.GetPrompt(ctx, g, "current_time", map[string]string{"timezone": "UTC"})
if err != nil {
log.Fatal(err)
}
resp, err := prompt.Execute(ctx, ai.WithModelName("googleai/gemini-flash-latest"))
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Text())

With MCPHost, you can dynamically manage server connections:

// Connect to a new server at runtime
err = host.Connect(ctx, g, "weather", mcp.MCPClientOptions{
Name: "weather-server",
Stdio: &mcp.StdioConfig{
Command: "python",
Args: []string{"weather_server.py"},
},
})
if err != nil {
log.Fatal(err)
}
// Restart one server's connection
err = host.Reconnect(ctx, "weather")
if err != nil {
log.Fatal(err)
}
// Get all tools from all active servers
tools, err := host.GetActiveTools(ctx, g)
if err != nil {
log.Fatal(err)
}
// Get a specific prompt from a specific server
prompt, err := host.GetPrompt(ctx, g, "mcp-server-time", "current_time", nil)
if err != nil {
log.Fatal(err)
}
// Disconnect a server completely, dropping it from the host
err = host.Disconnect(ctx, "weather")
if err != nil {
log.Fatal(err)
}

MCPHost exposes no accessor for the clients it owns, so per-server control is limited to Connect, Reconnect, and Disconnect. If you need per-client control, build the clients yourself with NewGenkitMCPClient and keep the references. Note that client.Disable() closes the connection, which kills a stdio child, and client.Reenable() reconnects. There is no way to keep a connection open while suppressing its tools.

Manage connection lifecycle and signal handling explicitly during application shutdown:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))
client, err := mcp.NewGenkitMCPClient(mcp.MCPClientOptions{ /* ... */ })
if err != nil {
log.Fatal(err)
}
defer client.Disconnect() // closes the transport, which reaps the stdio child

For an MCPHost, call host.Disconnect(ctx, name) for each registered server during shutdown to ensure stdio child processes and network transports are cleanly terminated.

The rest of the operational picture:

  • Timeouts. Per-request calls (GetActiveTools, GetActiveResources, GetPrompt, and tool execution) take a context and honor it, so wrap them in context.WithTimeout. Connection setup does not: NewGenkitMCPClient takes no context and starts the transport with a background context, so a stdio child that never speaks blocks indefinitely. StreamableHTTPConfig.Timeout bounds individual HTTP requests; StdioConfig and SSEConfig have no timeout field.
  • Concurrency. Host and client management operations (Connect, Disconnect, Reconnect, Disable, Reenable) mutate connection state. Initialize connections during application startup, or protect dynamic connection changes with synchronization. Read operations during active flows are safe once connections are established.
  • Reconnection. Connection recovery is managed explicitly using host.Reconnect(ctx, name) or client.Restart(ctx).
  • Failed handshakes. If the MCP initialize exchange fails, the error is recorded on the connection rather than returned: NewGenkitMCPClient still gives you a client, and GetActiveTools then returns an empty list with no error. Call GetActiveTools right after construction to confirm the server really answered.

mcp.NewMCPServer returns a GenkitMCPServer that publishes every tool defined with genkit.DefineTool and every resource registered on the Genkit instance. It discovers them from the registry, so you do not list them anywhere:

package main
import (
"context"
"log"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/mcp"
)
type addInput struct {
A int `json:"a"`
B int `json:"b"`
}
func main() {
ctx := context.Background()
g := genkit.Init(ctx)
// NewMCPServer picks up every tool already defined on g, so this value
// does not need to be referenced again.
genkit.DefineTool(g, "add", "Add two numbers",
func(ctx *ai.ToolContext, in addInput) (int, error) {
return in.A + in.B, nil
})
srv := mcp.NewMCPServer(g, mcp.MCPServerOptions{
Name: "genkit-calculator",
Version: "1.0.0",
})
log.Println("starting MCP server on stdio")
if err := srv.ServeStdio(); err != nil {
log.Fatal(err)
}
}

Flows are not published. To expose a flow over MCP, wrap it in a tool defined with genkit.DefineTool. The list_flows and run_flow tools described on the Genkit MCP server page belong to the Genkit CLI’s own server, not to yours.

The server speaks stdio only. Serve(transport) ignores its argument and calls ServeStdio regardless, and Close() is currently a no-op. GetServer() returns the underlying mcp-go server, but it is nil until ServeStdio has run the server’s setup pass, so it is not usable as an HTTP escape hatch. ListRegisteredTools and ListRegisteredResources are empty for the same reason until the server starts.

MCPClientOptions has three transport fields. Set exactly one.

FieldUse
Stdio *StdioConfigStart a local server process and speak over its stdin and stdout.
StreamableHTTP *StreamableHTTPConfigConnect to a remote server over Streamable HTTP. This is the current HTTP transport in the MCP specification.
SSE *SSEConfigConnect to a remote server over HTTP with server-sent events. This is the legacy HTTP transport; prefer Streamable HTTP for new servers.
Stdio: &mcp.StdioConfig{
Command: "uvx",
Args: []string{"mcp-server-time"},
Env: []string{"DEBUG=1"},
}

Headers are sent on every request, which is how you reach an authenticated server:

client, err := mcp.NewGenkitMCPClient(mcp.MCPClientOptions{
Name: "docs-server",
StreamableHTTP: &mcp.StreamableHTTPConfig{
BaseURL: "https://mcp.example.com/mcp",
Headers: map[string]string{"Authorization": "Bearer " + os.Getenv("MCP_TOKEN")},
Timeout: 30 * time.Second,
},
})

SSEConfig takes the same headers but has no Timeout field. Set the timeout on a custom HTTPClient, which is also where custom TLS or instrumentation goes:

client, err := mcp.NewGenkitMCPClient(mcp.MCPClientOptions{
Name: "legacy-server",
SSE: &mcp.SSEConfig{
BaseURL: "https://mcp.example.com/sse",
Headers: map[string]string{"Authorization": "Bearer " + os.Getenv("MCP_TOKEN")},
HTTPClient: &http.Client{Timeout: 30 * time.Second},
},
})

To test your Genkit application as an MCP server:

Terminal window
# Run your server
go run main.go
# Test with MCP Inspector in another terminal
npx @modelcontextprotocol/inspector go run main.go
type MCPClientOptions struct {
Name string // Client name; also the namespace prefix (defaults to "unnamed")
Version string // Version number (defaults to "1.0.0")
Disabled bool // Temporarily disable this client
Stdio *StdioConfig // Stdio transport config
SSE *SSEConfig // SSE transport config (legacy HTTP transport)
StreamableHTTP *StreamableHTTPConfig // Streamable HTTP transport config
}
type StdioConfig struct {
Command string // Command to run
Env []string // Extra environment variables, in KEY=VALUE form
Args []string // Command arguments
}

Env is appended to the parent process environment (os.Environ()), not a replacement for it, so PATH and everything else is inherited and uvx resolves. A duplicate key overrides the inherited value.

type SSEConfig struct {
BaseURL string // SSE endpoint, for example https://mcp.example.com/sse
Headers map[string]string // Sent on every request; use for Authorization or API keys
HTTPClient *http.Client // Optional; set the timeout, TLS config, or instrumentation here
}

SSEConfig has no Timeout field. Set it on HTTPClient.

type StreamableHTTPConfig struct {
BaseURL string // Endpoint, for example https://mcp.example.com/mcp
Headers map[string]string // Sent on every request; use for Authorization or API keys
HTTPClient *http.Client // Currently ignored by this transport; use Timeout instead
Timeout time.Duration // Per-request HTTP timeout
}

The Streamable HTTP transport applies Headers and Timeout only. HTTPClient is accepted but not wired up, so custom TLS or instrumentation needs the SSE transport today.

type MCPServerConfig struct {
Name string // Name for this server
Config MCPClientOptions // Client configuration options
}
type MCPHostOptions struct {
Name string // Host instance name
Version string // Host version (defaults to "1.0.0")
MCPServers []MCPServerConfig // Array of server configurations
}
type MCPServerOptions struct {
Name string // Server name
Version string // Server version
}