MCPExamples

Choose documentation 3

latest

Current version

latest
English

Prerelease This site documents an alpha of libtmux. Its structure, URLs and APIs are subject to change.

Edit this page on GitHub

List sessions through MCP

github.com/libtmux/libtmux-go/mcp · Source

This program creates a private tmux session, launches libtmux-mcp as a child process with the inspect toolset, and calls list_sessions. It checks the returned session ID against the session it created, then closes the client and stops its server.

Run the example

Use Go 1.26 or newer and tmux 3.2a or newer on Linux, macOS, or WSL. The project pins the core library and MCP executable to their published releases. The client uses the official Go MCP SDK.

Create an empty directory:

Terminal window
$ mkdir inspect-mcp-sessions

Enter it:

Terminal window
$ cd inspect-mcp-sessions

Install the MCP executable inside the project:

Terminal window
$ GOBIN="$PWD/.tools" go install \
github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@v0.0.1-alpha.12

Save the module file:

go.mod
module example.com/session-inspector
go 1.26.0
require (
github.com/libtmux/libtmux-go v0.0.1-alpha.9
github.com/modelcontextprotocol/go-sdk v1.6.1
)
require (
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)

Client program

Save this as main.go:

main.go
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/libtmux/libtmux-go/tmux"
sdk "github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() (result error) {
directory, err := os.MkdirTemp("", "libtmux-go-mcp-")
if err != nil {
return err
}
server, err := tmux.NewServer(tmux.ServerOptions{
SocketPath: filepath.Join(directory, "tmux.sock"),
ConfigFile: "/dev/null",
})
if err != nil {
return errors.Join(err, os.RemoveAll(directory))
}
defer func() {
cleanup, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Kill(cleanup); err != nil {
result = errors.Join(result, fmt.Errorf(
"stop tmux; resources retained at %s: %w", directory, err,
))
return
}
result = errors.Join(result, os.RemoveAll(directory))
}()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
created, err := server.NewSession(ctx, tmux.NewSessionRequest{
Name: "demo",
Command: "sh",
})
if err != nil {
return err
}
binary, err := filepath.Abs(".tools/libtmux-mcp")
if err != nil {
return err
}
command := exec.Command(binary,
"-socket-path", server.SocketPath(), "-binary", server.Executable(),
)
command.Stderr = os.Stderr
command.Env = []string{
"PATH=" + os.Getenv("PATH"),
"HOME=" + os.Getenv("HOME"),
"LANG=C.UTF-8",
"TMPDIR=" + directory,
"LIBTMUX_TOOLSETS=inspect",
}
client := sdk.NewClient(&sdk.Implementation{
Name: "session-inspector", Version: "1.0.0",
}, nil)
connection, err := client.Connect(ctx, &sdk.CommandTransport{
Command: command, TerminateDuration: 3 * time.Second,
}, nil)
if err != nil {
return fmt.Errorf("connect MCP client: %w", err)
}
defer func() {
if err := connection.Close(); err != nil {
result = errors.Join(result, fmt.Errorf("close MCP client: %w", err))
}
}()
reply, err := connection.CallTool(ctx, &sdk.CallToolParams{
Name: "list_sessions", Arguments: map[string]any{},
})
if err != nil {
return fmt.Errorf("call list_sessions: %w", err)
}
if reply.IsError {
for _, content := range reply.Content {
if text, ok := content.(*sdk.TextContent); ok {
return fmt.Errorf("list_sessions: %s", text.Text)
}
}
return errors.New("list_sessions failed without a text explanation")
}
data, err := json.Marshal(reply.StructuredContent)
if err != nil {
return err
}
var listed struct {
Sessions []struct {
ID string `json:"id"`
Name string `json:"name"`
Windows int `json:"windows"`
Attached int `json:"attached"`
} `json:"sessions"`
}
if err := json.Unmarshal(data, &listed); err != nil {
return fmt.Errorf("decode sessions: %w", err)
}
if len(listed.Sessions) != 1 || listed.Sessions[0].ID != string(created.ID()) {
return fmt.Errorf("unexpected session listing: %s", data)
}
session := listed.Sessions[0]
fmt.Printf("%s: %d window(s), %d attached client(s)\n",
session.Name, session.Windows, session.Attached,
)
return nil
}

Resolve the module dependencies:

Terminal window
$ go mod tidy

Run the program from the directory containing both files and .tools:

Terminal window
$ go run .

Its stdout is:

demo: 1 window(s), 0 attached client(s)

Read the result

tmux.NewServer configures the private endpoint, and Server.NewSession starts the daemon with an empty tmux configuration. The MCP child connects to that exact socket using the same tmux executable.

The SDK’s CallTool can return a protocol error before there is a tool result. A returned reply can also set IsError; the program checks both before reading StructuredContent. For this tool, structured content contains a sessions array with id, name, windows, and attached fields.

The example decodes only the fields it needs and verifies that exactly one session is returned with the expected ID. It does not assume that the first item belongs to it. See the tool reference for all returned fields and the session-list handler for the selected release’s implementation.

Shutdown

Startup and requests share a 20-second context. During shutdown, the client closes the child’s stdin, then escalates to SIGTERM and SIGKILL if needed. It waits up to three seconds after each step. Deferred cleanup closes the client before stopping the owned tmux server with a fresh five-second context, so an expired request context does not skip daemon cleanup.

The child receives a curated environment with the selected socket and tmux binary. Its temporary files live under the program’s owned directory through TMPDIR. It does not inherit TMUX, TMUX_PANE, or unrelated LIBTMUX_* settings from your interactive shell.

The program joins operation and cleanup errors. It removes its temporary directory only after Server.Kill succeeds; otherwise, it prints the retained directory for inspection. Its server cleanup implementation defines the library’s kill behavior.

The connection guide covers connecting to an existing server instead. The example’s cleanup is appropriate because it creates its own private socket and session.

Esc

Type to search.