Skip to main content

Model Context Protocol (MCP)

The Model Context Protocol (MCP) is an open standard that lets applications expose tools and resources to LLM clients over a uniform JSON-RPC interface. Chronos ships an MCP client so an agent can connect to any MCP server, import its tools into the agent's tool registry, and let the model call them transparently — the same way it calls native Chronos tools.

This means you can plug in the growing ecosystem of MCP servers (filesystem, GitHub, Postgres, Slack, Puppeteer, and many more) without writing a single tool handler.

How it works

┌────────────┐ tools/list, tools/call ┌──────────────────┐
│ Chronos │ ──────────────────────────▶ │ MCP Server │
│ Agent │ JSON-RPC 2.0 over stdio │ (subprocess) │
│ │ ◀────────────────────────── │ filesystem, ... │
└────────────┘ results └──────────────────┘
  1. You register one or more MCP servers on the agent.
  2. ConnectMCP launches each server, performs the MCP initialize handshake, and calls tools/list.
  3. Every advertised tool is wrapped as a tool.Definition and registered in the agent's registry. Its handler routes tools/call requests back through the MCP client.
  4. From then on, the model sees MCP tools alongside native tools and can call them during Chat, Run, or graph execution.

Transports

TransportConstStatusUse case
stdiomcp.TransportStdio✅ SupportedLaunch a local MCP server as a subprocess (default)
HTTP SSEmcp.TransportSSE✅ SupportedConnect to a remote MCP server over HTTP (MCP 2024-11-05)

stdio is the default (used when Transport is empty). The SSE transport opens a long-lived Server-Sent Events stream to the server's URL, learns the endpoint the server advertises, and POSTs JSON-RPC requests to it — responses are correlated back over the stream by id. Per-call timeouts are honored and CloseMCP cancels the stream and closes idle connections.

// Connect to a remote MCP server over HTTP + SSE.
builder.AddMCPServer(mcp.ServerConfig{
Name: "remote-tools",
Transport: mcp.TransportSSE,
URL: "https://mcp.example.com/sse", // required for SSE
})

Go builder API

Add an MCP server while building the agent, then call ConnectMCP once after Build:

package main

import (
"context"
"log"
"os"

"github.com/spawn08/chronos/engine/mcp"
"github.com/spawn08/chronos/engine/model"
"github.com/spawn08/chronos/sdk/agent"
)

func main() {
ctx := context.Background()

a, err := agent.New("assistant", "Assistant").
WithModel(model.NewOpenAI(os.Getenv("OPENAI_API_KEY"))).
WithSystemPrompt("You are a helpful assistant with filesystem access.").
AddMCPServer(mcp.ServerConfig{
Name: "filesystem",
Transport: mcp.TransportStdio,
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "."},
}).
Build()
if err != nil {
log.Fatal(err)
}

// Launch servers and import their tools into the registry.
if err := a.ConnectMCP(ctx); err != nil {
log.Fatal(err)
}
defer a.CloseMCP() // shut down MCP subprocesses

resp, err := a.Chat(ctx, "List the files in the current directory.")
if err != nil {
log.Fatal(err)
}
log.Println(resp.Content)
}

Builder methods

MethodSignatureDescription
AddMCPServerAddMCPServer(cfg mcp.ServerConfig) *BuilderRegister an MCP server on the agent. Multiple servers can be added.
ConnectMCP(a *Agent) ConnectMCP(ctx context.Context) errorConnect all registered servers and import their tools. Call after Build.
CloseMCP(a *Agent) CloseMCP()Disconnect all servers and terminate their subprocesses.
note

ConnectMCP is a separate step (not part of Build) because it performs I/O — it launches processes and blocks on the initialize handshake. Always pair it with defer a.CloseMCP().

YAML configuration

MCP servers are first-class in .chronos/agents.yaml. List them under mcp_servers on any agent. Command, args, and URL support ${ENV_VAR} expansion.

# .chronos/agents.yaml
agents:
- id: assistant
name: Assistant
model:
provider: openai
model: gpt-5.5
api_key: ${OPENAI_API_KEY}
system_prompt: You are a helpful assistant.
mcp_servers:
- name: filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
- name: github
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]

mcp_servers fields

FieldYAML keyDescription
NamenameLogical name for the server (used in error messages).
Transporttransportstdio (default) or sse (planned).
CommandcommandExecutable to launch for stdio transport (e.g. npx, uvx, a binary path).
ArgsargsArguments passed to the command.
URLurlEndpoint for SSE transport (when supported).

When you build agents with agent.BuildAgent / agent.BuildAll, the servers are registered automatically. You still call ConnectMCP before running:

fc, _ := agent.LoadFile(".chronos/agents.yaml")
agents, _ := agent.BuildAll(ctx, fc)
a := agents["assistant"]
if err := a.ConnectMCP(ctx); err != nil {
log.Fatal(err)
}
defer a.CloseMCP()

Working with resources

Beyond tools, MCP servers can expose resources — readable content addressed by URI (files, database rows, API responses). The client exposes these directly:

client, _ := mcp.NewClient(mcp.ServerConfig{
Name: "filesystem",
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "."},
})
if err := client.Connect(ctx); err != nil {
log.Fatal(err)
}
defer client.Close()

// Discover resources
resources, _ := client.ListResources(ctx)
for _, r := range resources {
fmt.Printf("%s (%s)\n", r.URI, r.MimeType)
}

// Read one
contents, _ := client.ReadResource(ctx, "file:///path/to/README.md")
for _, c := range contents {
fmt.Println(c.Text)
}

Client methods

MethodDescription
NewClient(cfg ServerConfig) (*Client, error)Create a client for a server config.
Connect(ctx) errorLaunch the server and run the initialize handshake.
ListTools(ctx) ([]ToolInfo, error)Fetch advertised tools (tools/list).
CallTool(ctx, name, args) (any, error)Invoke a tool (tools/call).
ListResources(ctx) ([]ResourceInfo, error)Fetch resources (resources/list).
ReadResource(ctx, uri) ([]ResourceContent, error)Read a resource (resources/read).
Info() ServerInfoServer name/version/protocol from the handshake.
Close() errorTerminate the server subprocess.

Registering tools manually

If you want more control (e.g. filter or rename tools before registering), use the adapter directly:

import "github.com/spawn08/chronos/engine/mcp"

// Register every tool from a connected client into a registry:
n, err := mcp.RegisterTools(ctx, client, agent.Tools)

// Or convert to definitions without registering, for inspection:
tools, _ := client.ListTools(ctx)
defs := mcp.ToolInfoToDefinitions(client, tools)

Permissions & approval

Imported MCP tools are ordinary tool.Definition entries, so they participate in the same permission and approval flow as native tools.

MCP tools are registered with tool.PermRequireApproval by default — because they come from an external server, they route through the human-approval path unless you explicitly opt out. To auto-allow a specific MCP tool you trust, look it up after ConnectMCP and relax its policy:

if def, ok := a.Tools.Get("list_files"); ok {
def.Permission = tool.PermAllow // trust this read-only MCP tool
}

Conversely, the default already requires approval, so no action is needed to gate a destructive tool like write_file.

Example

A complete runnable example lives in examples/mcp_agent. It connects to the filesystem MCP server, prints the imported tools, and (with an API key set) lets the model use them:

go run ./examples/mcp_agent/

Being an MCP server

Chronos can also expose its own tools to any MCP host — Claude Desktop, an IDE, or another agent framework — with the engine/interop/mcpserver package. Where the client above consumes external servers, this lets Chronos be one.

import (
"github.com/spawn08/chronos/engine/interop/mcpserver"
"github.com/spawn08/chronos/engine/tool"
)

srv := mcpserver.New("chronos-tools", mcpserver.WithVersion("1.0.0"))
srv.Expose(addTool) // a *tool.Definition
srv.ExposeAll(registry) // or a whole tool.Registry
srv.SetApprover(approver) // human-in-the-loop for guarded tools

// stdio (for a host that launches the process):
srv.ServeStdio(ctx, os.Stdin, os.Stdout)

// or HTTP+SSE (mount the handler on your server):
http.Handle("/mcp", srv.SSEHandler())

A tools/call is dispatched through the underlying tool.Registry, so it automatically honors each tool's permission, the approval hook, and the panic-to-error recovery already built into the engine — a panicking tool fails only its own call.

Safe by default: a tool exposed without an explicit permission defaults to PermRequireApproval, so a remote host cannot silently run it without human approval. Expose a tool as tool.PermAllow to opt it into auto-approval, or change the default with mcpserver.WithDefaultPermission. Denied (PermDeny) tools are never advertised.

Both transports share one transport-agnostic dispatcher (HandleMessage), and the SSE transport interoperates with the Chronos MCP client above. A complete runnable example is in examples/mcp_server.

Troubleshooting

SymptomCauseFix
mcp: command is required for stdio transportNo command setProvide command (and args) in the server config.
mcp connect "...": ... executable file not foundServer binary not installedInstall the MCP server (e.g. npm i -g @modelcontextprotocol/server-filesystem) or ensure npx/uvx is on PATH.
mcp: url is required for sse transportSSE selected without a URLSet url in the server config for sse transport.
Tools don't appearConnectMCP not calledCall a.ConnectMCP(ctx) after Build.

See also