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.
Prerequisites
Section titled “Prerequisites”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
Configuration
Section titled “Configuration”Connecting to a single server
Section titled “Connecting to a single server”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.
Multiple server management
Section titled “Multiple server management”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)}Using tools from MCP servers
Section titled “Using tools from MCP servers”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.
Tool, prompt, and resource names
Section titled “Tool, prompt, and resource names”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)}Tool results
Section titled “Tool results”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 valueif !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.
Using resources from MCP servers
Section titled “Using resources from MCP servers”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})Using prompts from MCP servers
Section titled “Using prompts from MCP servers”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())Managing multiple servers
Section titled “Managing multiple servers”With MCPHost, you can dynamically manage server connections:
// Connect to a new server at runtimeerr = 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 connectionerr = host.Reconnect(ctx, "weather")if err != nil { log.Fatal(err)}
// Get all tools from all active serverstools, err := host.GetActiveTools(ctx, g)if err != nil { log.Fatal(err)}
// Get a specific prompt from a specific serverprompt, 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 hosterr = 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.
Lifecycle and production concerns
Section titled “Lifecycle and production concerns”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 childFor 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 incontext.WithTimeout. Connection setup does not:NewGenkitMCPClienttakes no context and starts the transport with a background context, so a stdio child that never speaks blocks indefinitely.StreamableHTTPConfig.Timeoutbounds individual HTTP requests;StdioConfigandSSEConfighave 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)orclient.Restart(ctx). - Failed handshakes. If the MCP
initializeexchange fails, the error is recorded on the connection rather than returned:NewGenkitMCPClientstill gives you a client, andGetActiveToolsthen returns an empty list with no error. CallGetActiveToolsright after construction to confirm the server really answered.
Running as an MCP server
Section titled “Running as an MCP server”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.
Transport options
Section titled “Transport options”MCPClientOptions has three transport fields. Set exactly one.
| Field | Use |
|---|---|
Stdio *StdioConfig | Start a local server process and speak over its stdin and stdout. |
StreamableHTTP *StreamableHTTPConfig | Connect to a remote server over Streamable HTTP. This is the current HTTP transport in the MCP specification. |
SSE *SSEConfig | Connect 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"},}Streamable HTTP
Section titled “Streamable HTTP”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}, },})Testing
Section titled “Testing”Testing your MCP server
Section titled “Testing your MCP server”To test your Genkit application as an MCP server:
# Run your servergo run main.go
# Test with MCP Inspector in another terminalnpx @modelcontextprotocol/inspector go run main.goConfiguration options
Section titled “Configuration options”MCPClientOptions
Section titled “MCPClientOptions”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}StdioConfig
Section titled “StdioConfig”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.
SSEConfig
Section titled “SSEConfig”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.
StreamableHTTPConfig
Section titled “StreamableHTTPConfig”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.
MCPServerConfig
Section titled “MCPServerConfig”type MCPServerConfig struct { Name string // Name for this server Config MCPClientOptions // Client configuration options}MCPHostOptions
Section titled “MCPHostOptions”type MCPHostOptions struct { Name string // Host instance name Version string // Host version (defaults to "1.0.0") MCPServers []MCPServerConfig // Array of server configurations}MCPServerOptions
Section titled “MCPServerOptions”type MCPServerOptions struct { Name string // Server name Version string // Server version}