# MCP Integration

Create MCP servers with tools, resources, and prompts, and consume external MCP tools.

Source: https://goa.design/docs/2-goa-ai/mcp-integration/

Relative links resolve against the source URL above.


Goa-AI supports both **creating MCP servers** and **consuming external MCP tools**. Add MCP declarations to a Goa service to expose methods as tools, publish resources, and provide prompt templates. The generator produces JSON-RPC protocol handling and service adapters. Hosting an MCP server does not require running a Goa-AI agent.

The agent-side HTTP and stdio callers consume tools: they initialize a session, discover the server’s tools capability, and invoke `tools/call`. Their tool-calling interface does not expose resource or prompt operations. The generated MCP server supports the tools, resources, and prompts declared in its design.

## Overview

MCP integration follows this workflow:

1. **Service design**: Declare the MCP server via Goa's MCP DSL
2. **Agent design**: Reference that suite via a toolset declared with `FromMCP(...)` or `FromExternalMCP(...)`
3. **Code generation**: Produces the MCP JSON-RPC server (when Goa-backed) plus runtime registration helpers and toolset-owned specs/codecs for the suite
4. **Runtime wiring**: Instantiate an HTTP or stdio `mcpruntime.Caller`. The
   HTTP caller accepts either a JSON response or an HTTP event stream. Generated
   helpers register the toolset and adapt JSON-RPC errors into
   `planner.ToolFailure` values
5. **Planner execution**: Planners construct calls with generated typed tool
   descriptors; the runtime forwards canonical JSON to the MCP caller, records
   results, and surfaces structured telemetry

---

## Declaring MCP Toolsets

### In Service Design

First, declare the MCP server in your Goa service design:

```go
package design

import (
    . "goa.design/goa/v3/dsl"
    . "goa.design/goa-ai/dsl"
)

var _ = Service("assistant", func() {
    Description("MCP server for assistant tools")
    
    MCP("assistant-mcp", "1.0.0")
    JSONRPC(func() {
        POST("/mcp")
    })
    
    StaticPrompt("find-docs", "Help a user find documentation",
        "user", "Find relevant documentation for the user's question.")

    Method("readme", func() {
        Result(String)
        Resource("readme", "file:///docs/README.md", "text/markdown")
    })

    Method("search", func() {
        Payload(func() {
            Attribute("query", String, "Search query")
            Required("query")
        })
        Result(func() {
            Attribute("results", ArrayOf(String), "Search results")
            Required("results")
        })
        Tool("search", "Search documents by query")
    })
})
```

### In Agent Design

Then reference the MCP suite in your agent:

```go
var AssistantSuite = Toolset(FromMCP("assistant", "assistant-mcp"))

var _ = Service("orchestrator", func() {
    Agent("chat", "Conversational runner", func() {
        Use(AssistantSuite)
        RunPolicy(func() {
            DefaultCaps(MaxToolCalls(8))
            TimeBudget("2m")
        })
    })
})
```

### External MCP Servers with Inline Schemas

For external MCP servers (not Goa-backed), declare tools with inline schemas:

```go
var RemoteSearch = Toolset("remote-search", FromExternalMCP("remote", "search"), func() {
    Tool("web_search", "Search the web", func() {
        Args(func() { Attribute("query", String) })
        Return(func() { Attribute("results", ArrayOf(String)) })
    })
})

Agent("helper", "", func() {
    Use(RemoteSearch)
})
```

---

## Runtime Wiring

At runtime, instantiate an MCP caller and register the toolset:

```go
import (
    mcpruntime "goa.design/goa-ai/runtime/mcp"
    mcpassistant "example.com/assistant/gen/assistant/mcp_assistant"
)

// Create an HTTP MCP caller.
caller, err := mcpruntime.NewHTTPCaller(ctx, mcpruntime.HTTPOptions{
    Endpoint: "https://assistant.example.com/mcp",
    ClientInfo: mcpruntime.ClientInfo{
        Name:    "my-agent",
        Version: "1.0.0",
    },
})
if err != nil {
    log.Fatal(err)
}

// Register the MCP toolset
if err := mcpassistant.RegisterAssistantAssistantMcpToolset(ctx, rt, caller); err != nil {
    log.Fatal(err)
}
```

---

## MCP Caller Types

Goa-AI supports HTTP and stdio through the `runtime/mcp` package. Both callers
implement the `Caller` interface:

```go
type Caller interface {
    CallTool(ctx context.Context, req CallRequest) (CallResponse, error)
}

type CallRequest struct {
    Tool    string
    Payload json.RawMessage
}

type CallResponse struct {
    Content           []ContentBlock
    StructuredContent json.RawMessage
}
```

### HTTP Caller

For MCP servers accessible via HTTP JSON-RPC:

```go
import mcpruntime "goa.design/goa-ai/runtime/mcp"

caller, err := mcpruntime.NewHTTPCaller(ctx, mcpruntime.HTTPOptions{
    Endpoint: "https://assistant.example.com/mcp",
    Client:   customHTTPClient, // Optional; defaults to a client with a 30-second timeout.
    ClientInfo: mcpruntime.ClientInfo{
        Name:    "my-agent",
        Version: "1.0.0",
    },
    InitTimeout: 10 * time.Second, // Optional initialization timeout.
})
```

The HTTP caller performs the MCP initialize handshake on creation. It sends
each JSON-RPC 2.0 message as an HTTP `POST` to the configured endpoint. Tool
responses may be JSON or an HTTP event stream; a separate SSE caller is not
needed.

### Stdio Caller

For MCP servers running as subprocesses communicating via stdin/stdout:

```go
import mcpruntime "goa.design/goa-ai/runtime/mcp"

caller, err := mcpruntime.NewStdioCaller(ctx, mcpruntime.StdioOptions{
    Command: "mcp-server",
    Args:    []string{"--config", "config.json"},
    Env:     []string{"MCP_DEBUG=1"}, // Added to the current environment.
    Dir:     "/path/to/workdir",
    ClientInfo: mcpruntime.ClientInfo{
        Name:    "my-agent",
        Version: "1.0.0",
    },
    InitTimeout: 10 * time.Second, // Optional initialization timeout.
})
defer caller.Close() // Clean up subprocess
```

The stdio caller launches the command as a subprocess, performs the MCP initialize handshake, and maintains the session across tool invocations. Call `Close()` to terminate the subprocess when done.

### CallerFunc Adapter

For custom caller implementations or testing:

```go
import mcpruntime "goa.design/goa-ai/runtime/mcp"

// Adapt a function to the Caller interface
caller := mcpruntime.CallerFunc(func(ctx context.Context, req mcpruntime.CallRequest) (mcpruntime.CallResponse, error) {
    content, structured, err := myCustomMCPCall(ctx, req.Tool, req.Payload)
    if err != nil {
        return mcpruntime.CallResponse{}, err
    }
    return mcpruntime.CallResponse{
        Content:           content,
        StructuredContent: structured,
    }, nil
})
```

### Goa-Generated JSON-RPC Caller

For Goa-generated MCP clients that wrap service methods:

```go
caller, err := mcpassistant.NewCaller(ctx, client, mcpruntime.ClientInfo{
    Name:    "my-agent",
    Version: "1.0.0",
})
```

---

## Tool Execution Flow

1. Planner returns tool calls constructed from the generated MCP tool
   descriptors, or forwards validated model calls with
   `planner.ToolRequestFromModelCall`
2. Runtime validates the complete planner result and assigns execution IDs,
   producing `runtime.ToolCall` values
3. Runtime detects MCP toolset registration
4. Forwards the runtime call's canonical JSON payload to the MCP caller
5. The MCP caller uses HTTP or stdio and handles the JSON-RPC protocol. An HTTP
   response may be JSON or an event stream
6. Decodes result using generated codec
7. Returns `ToolResult` to planner

---

## Error Handling

Generated helpers adapt JSON-RPC errors into `planner.ToolFailure` values:

- **Validation errors** → invalid-call failures with exact correction evidence
- **Network errors** → unavailable or timeout failures with an explicit
  replanning or finish action
- **Server errors** → structured causes preserved in the failure

This gives MCP and native toolsets the same enforced recovery contract.

Failures returned by a tool become `ToolFailure`. An invalid completed planner
result becomes `OutputContractError` instead; it is rejected without another
model request and is not presented as a tool failure.

---

## Complete Example

### Design

```go
package design

import (
    . "goa.design/goa/v3/dsl"
    . "goa.design/goa-ai/dsl"
)

// MCP server service
var _ = Service("assistant", func() {
    Description("MCP server for assistant tools")
    
    MCP("assistant-mcp", "1.0.0")
    JSONRPC(func() {
        POST("/mcp")
    })
    
    Method("search", func() {
        Payload(func() {
            Attribute("query", String, "Search query")
            Required("query")
        })
        Result(func() {
            Attribute("results", ArrayOf(String), "Search results")
            Required("results")
        })
        Tool("search", "Search documents by query")
    })
})

// Agent that uses MCP tools
var AssistantSuite = Toolset(FromMCP("assistant", "assistant-mcp"))

var _ = Service("orchestrator", func() {
    Agent("chat", "Conversational runner", func() {
        Use(AssistantSuite)
        RunPolicy(func() {
            DefaultCaps(MaxToolCalls(8))
            TimeBudget("2m")
        })
    })
})
```

### Runtime

```go
package main

import (
    "context"
    "log"
    
    mcpruntime "goa.design/goa-ai/runtime/mcp"
    chat "example.com/assistant/gen/orchestrator/agents/chat"
    mcpassistant "example.com/assistant/gen/assistant/mcp_assistant"
    "goa.design/goa-ai/runtime/agent/runtime"
    storageinmem "goa.design/goa-ai/runtime/agent/storage/inmem"
)

func main() {
    rt := runtime.New(storageinmem.New())
    ctx := context.Background()
    
    // Wire MCP caller
    caller, err := mcpruntime.NewHTTPCaller(ctx, mcpruntime.HTTPOptions{
        Endpoint: "https://assistant.example.com/mcp",
        ClientInfo: mcpruntime.ClientInfo{
            Name:    "my-agent",
            Version: "1.0.0",
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    if err := mcpassistant.RegisterAssistantAssistantMcpToolset(ctx, rt, caller); err != nil {
        log.Fatal(err)
    }
    
    // Register agent
    if err := chat.RegisterChatAgent(ctx, rt, chat.ChatAgentConfig{
        Planner: &MyPlanner{},
    }); err != nil {
        log.Fatal(err)
    }
    
    // Run agent
    client := chat.NewClient(rt)
    // ... use client ...
}
```

### Planner

Your planner can reference MCP tools just like native toolsets:

```go
func (p *MyPlanner) PlanStart(ctx context.Context, in *planner.PlanInput) (*planner.PlanResult, error) {
    call, err := planner.NewToolRequest(
        mcpspecs.SearchTool(),
        &mcpspecs.SearchPayload{Query: "golang tutorials"},
    )
    if err != nil {
        return nil, err
    }
    return &planner.PlanResult{
        ToolCalls: []planner.ToolRequest{call},
    }, nil
}
```

Here `mcpspecs` is the generated specs package for the MCP toolset. When
forwarding a validated model-emitted tool call instead, use
`planner.ToolRequestFromModelCall` so its provider correlation ID is preserved.

---

## Best Practices

- **Let codegen manage registration**: Use the generated helper to register MCP
  toolsets; avoid hand-written glue so codecs and structured failure recovery
  stay consistent
- **Use typed callers**: Prefer Goa-generated JSON-RPC callers when available for type safety
- **Handle errors explicitly**: Map MCP errors to `ToolFailure` values with the
  correct failure kind and recovery action
- **Monitor telemetry**: MCP calls emit structured telemetry events; use them for observability
- **Choose the right transport**: Use HTTP for remote servers and stdio for subprocess-based servers. The HTTP caller accepts JSON and event-stream responses

---

## Next Steps

- **[Toolsets](./toolsets.md)** - Understand tool execution models
- **[Memory & Sessions](./memory-sessions.md)** - Manage state with transcripts and memory stores
- **[Production](./production.md)** - Deploy with Temporal and streaming UI

