| name | kit-sdk |
| description | Guide for building Go applications with the Kit SDK. Use when the user asks to create a program, service, script, or application that uses Kit programmatically as a Go library — e.g. embedding LLM interactions, building agents, creating CLI tools powered by Kit, or integrating Kit into backend services. Do NOT use for Kit extensions (use kit-extensions skill instead). |
Kit SDK Development Guide
The Kit SDK (pkg/kit) lets you embed Kit's full agent capabilities — LLM interactions, tool execution, session management, streaming, hooks — into any Go application. Unlike extensions (which are interpreted scripts running inside Kit's TUI), SDK programs are standalone compiled Go binaries.
Installation
go get github.com/mark3labs/kit
Import path (alias recommended):
import kit "github.com/mark3labs/kit/pkg/kit"
Quick Start
package main
import (
"context"
"fmt"
"log"
kit "github.com/mark3labs/kit/pkg/kit"
)
func main() {
ctx := context.Background()
host, err := kit.New(ctx, nil)
if err != nil {
log.Fatal(err)
}
defer func() { _ = host.Close() }()
response, err := host.Prompt(ctx, "What is 2+2?")
if err != nil {
log.Fatal(err)
}
fmt.Println(response)
}
Core Lifecycle
- Create:
kit.New(ctx, opts) — loads config, initializes MCP servers, creates LLM provider, sets up agent
- Interact:
host.Prompt(ctx, msg) — send messages, agent uses tools as needed
- Close:
host.Close() — cleans up MCP connections, model resources, session file handle
Always defer Close():
defer func() { _ = host.Close() }()
Options Reference
All fields are optional. Zero values use CLI defaults.
host, err := kit.New(ctx, &kit.Options{
Model: "anthropic/claude-sonnet-4-5-20250929",
SystemPrompt: "You are a helpful assistant",
ConfigFile: "/path/to/config.yml",
MaxSteps: 10,
Streaming: true,
Quiet: true,
Debug: true,
MaxTokens: 16384,
ThinkingLevel: "medium",
Temperature: ptrFloat32(0.2),
TopP: nil,
TopK: nil,
FrequencyPenalty: nil,
PresencePenalty: nil,
ProviderAPIKey: "sk-...",
ProviderURL: "https://proxy.internal/v1",
TLSSkipVerify: false,
SessionDir: "/path/to/project",
SessionPath: "/path/to/session.jsonl",
Continue: true,
NoSession: true,
SessionManager: myCustomSession,
Tools: []kit.Tool{kit.NewBashTool()},
ExtraTools: []kit.Tool{myTool},
DisableCoreTools: true,
SkipConfig: true,
Skills: []string{"/path/to/skill.md"},
SkillsDir: "/path/to/skills",
NoSkills: true,
NoExtensions: true,
NoContextFiles: true,
AutoCompact: true,
CompactionOptions: &kit.CompactionOptions{...},
MCPAuthHandler: mcpAuthHandler,
MCPTokenStoreFactory: func(serverURL string) (kit.MCPTokenStore, error) {
return myCustomStore(serverURL), nil
},
InProcessMCPServers: map[string]*kit.MCPServer{
"docs": mcpSrv,
},
})
func ptrFloat32(v float32) *float32 { return &v }
Critical distinction: Tools replaces ALL default tools (core + MCP + extension). ExtraTools adds tools alongside the defaults. Use Tools to restrict the agent's capabilities; use ExtraTools to extend them.
In-process MCP servers bypass subprocess spawning entirely. Pass *server.MCPServer instances from mcp-go via InProcessMCPServers or call AddInProcessMCPServer() at runtime.
Generation & provider Options (cheat sheet)
| Field | Type | Empty/nil means | Notes |
|---|
MaxTokens | int | Auto-resolve (env → config → per-model → 8192 floor) | Non-zero suppresses rightSizeMaxTokens |
ThinkingLevel | string | Auto-resolve (→ "off") | Valid: "off", "none", "minimal", "low", "medium", "high" |
Temperature | *float32 | Leave provider/per-model default | Pointer so explicit 0.0 ≠ unset |
TopP | *float32 | Leave provider/per-model default | |
TopK | *int32 | Leave provider/per-model default | |
FrequencyPenalty | *float32 | Leave provider/per-model default | OpenAI-family |
PresencePenalty | *float32 | Leave provider/per-model default | OpenAI-family |
ProviderAPIKey | string | Use config / provider env var | Overrides pre-existing viper state |
ProviderURL | string | Use provider default endpoint | Same base URL flag as --provider-url |
TLSSkipVerify | bool | — | Only effective when true; cannot force-disable via Options |
These fields eliminate the old viper.Set("max-tokens", 16384) dance many
downstream embedders used to do before calling kit.New(). Everything is
now discoverable via godoc on kit.Options.
Prompt Methods
Simple prompt — string in, string out
response, err := host.Prompt(ctx, "Explain this code")
Full result with usage stats
result, err := host.PromptResult(ctx, "Analyze this file")
Multimodal with file attachments
files := []kit.LLMFilePart{{
Filename: "screenshot.png",
MediaType: "image/png",
Data: imageBytes,
}}
result, err := host.PromptResultWithFiles(ctx, "What's in this image?", files)
Per-call system message injection
response, err := host.PromptWithOptions(ctx, "Review this PR", kit.PromptOptions{
SystemMessage: "Focus on security vulnerabilities only.",
})
System-level steering (no visible user message)
response, err := host.Steer(ctx, "Switch to a more formal tone")
Continue without new input
response, err := host.FollowUp(ctx, "")
Multiple user messages in one turn
result, err := host.PromptResultWithMessages(ctx, []string{
"Here is the code:",
"@file.go",
"Please review it.",
})
Event System
Events are read-only observations of the agent lifecycle. Register before calling Prompt.
Typed convenience subscribers
unsub := host.OnToolCall(func(e kit.ToolCallEvent) {
})
defer unsub()
host.OnToolCallStart(func(e kit.ToolCallStartEvent) {
})
host.OnToolCallDelta(func(e kit.ToolCallDeltaEvent) {
})
host.OnToolCallEnd(func(e kit.ToolCallEndEvent) {
})
host.OnToolResult(func(e kit.ToolResultEvent) {
})
host.OnToolOutput(func(e kit.ToolOutputEvent) {
})
host.OnMessageUpdate(func(e kit.MessageUpdateEvent) {
fmt.Print(e.Chunk)
})
host.OnResponse(func(e kit.ResponseEvent) {
})
host.OnTurnStart(func(e kit.TurnStartEvent) {
})
host.OnTurnEnd(func(e kit.TurnEndEvent) {
})
host.OnStepStart(func(e kit.StepStartEvent) {
})
host.OnStepFinish(func(e kit.StepFinishEvent) {
})
host.OnWarnings(func(e kit.WarningsEvent) {
for _, w := range e.Warnings {
log.Printf("warning: %s", w)
}
})
host.OnError(func(e kit.ErrorEvent) {
log.Printf("agent error: %v", e.Error)
})
host.OnRetry(func(e kit.RetryEvent) {
log.Printf("retrying (attempt %d): %v", e.Attempt, e.Error)
})
host.OnTextStart(func(e kit.TextStartEvent) {
})
host.OnTextEnd(func(e kit.TextEndEvent) {
})
host.OnReasoningStart(func(e kit.ReasoningStartEvent) {
})
host.OnSource(func(e kit.SourceEvent) {
})
host.OnStreamFinish(func(e kit.StreamFinishEvent) {
})
host.OnMessageStart(func(e kit.MessageStartEvent) {})
host.OnMessageEnd(func(e kit.MessageEndEvent) { })
host.OnReasoningDelta(func(e kit.ReasoningDeltaEvent) { })
host.OnReasoningComplete(func(e kit.ReasoningCompleteEvent) {})
host.OnToolExecutionStart(func(e kit.ToolExecutionStartEvent) { })
host.OnToolExecutionEnd(func(e kit.ToolExecutionEndEvent) { })
host.OnToolCallContent(func(e kit.ToolCallContentEvent) { })
host.OnStepUsage(func(e kit.StepUsageEvent) { })
host.OnCompaction(func(e kit.CompactionEvent) { })
host.OnSteerConsumed(func(e kit.SteerConsumedEvent) { })
Rename note: OnStreaming has been renamed to OnMessageUpdate. The old OnStreaming name is kept as a deprecated alias for one release cycle.
Generic subscriber (receives all events)
unsub := host.Subscribe(func(e kit.Event) {
switch ev := e.(type) {
case kit.ToolCallEvent:
case kit.MessageUpdateEvent:
case kit.CompactionEvent:
}
})
All event types
| Event Type | Struct | Key Fields |
|---|
turn_start | TurnStartEvent | Prompt |
turn_end | TurnEndEvent | Response, Error, StopReason |
message_start | MessageStartEvent | (none) |
message_update | MessageUpdateEvent | Chunk |
message_end | MessageEndEvent | Content |
tool_call_start | ToolCallStartEvent | ToolCallID, ToolName, ToolKind |
tool_call_delta | ToolCallDeltaEvent | ToolCallID, Delta |
tool_call_end | ToolCallEndEvent | ToolCallID |
tool_call | ToolCallEvent | ToolCallID, ToolName, ToolKind, ToolArgs, ParsedArgs |
tool_execution_start | ToolExecutionStartEvent | ToolCallID, ToolName, ToolKind, ToolArgs |
tool_execution_end | ToolExecutionEndEvent | ToolCallID, ToolName, ToolKind |
tool_result | ToolResultEvent | ToolCallID, ToolName, ToolKind, ToolArgs, ParsedArgs, Result, IsError, Metadata |
tool_call_content | ToolCallContentEvent | Content |
tool_output | ToolOutputEvent | ToolCallID, ToolName, Chunk, IsStderr |
response | ResponseEvent | Content |
compaction | CompactionEvent | Summary, OriginalTokens, CompactedTokens, MessagesRemoved, ReadFiles, ModifiedFiles |
reasoning_delta | ReasoningDeltaEvent | Delta |
step_usage | StepUsageEvent | InputTokens, OutputTokens, CacheReadTokens, CacheWriteTokens |
steer_consumed | SteerConsumedEvent | Count |
step_start | StepStartEvent | StepNumber |
step_finish | StepFinishEvent | StepNumber, HasToolCalls, FinishReason, Usage |
text_start | TextStartEvent | ID |
text_end | TextEndEvent | ID |
reasoning_start | ReasoningStartEvent | ID |
warnings | WarningsEvent | Warnings |
source | SourceEvent | SourceType, ID, URL, Title |
stream_finish | StreamFinishEvent | Usage, FinishReason |
error | ErrorEvent | Error |
retry | RetryEvent | Attempt, Error |
password_prompt | PasswordPromptEvent | Prompt, ResponseCh |
Tool call streaming lifecycle: ToolCallStartEvent → ToolCallDeltaEvent (repeated) → ToolCallEndEvent → ToolCallEvent → ToolExecutionStartEvent → ToolOutputEvent (optional, repeated) → ToolExecutionEndEvent → ToolResultEvent
PasswordPromptEvent (for sudo password handling):
type PasswordPromptEvent struct {
Prompt string
ResponseCh chan<- PasswordPromptResponse
}
type PasswordPromptResponse struct {
Password string
Cancelled bool
}
Tool kind constants
Tools are classified by kind for UI rendering:
ToolKindExecute = "execute" — bash
ToolKindEdit = "edit" — edit, write
ToolKindRead = "read" — read, ls
ToolKindSearch = "search" — grep, find
ToolKindSubagent = "agent" — subagent
Hook System (Interceptors)
Hooks can modify or cancel operations. Events are read-only; hooks are read-write.
BeforeToolCall — block tool execution
unsub := host.OnBeforeToolCall(kit.HookPriorityNormal, func(h kit.BeforeToolCallHook) *kit.BeforeToolCallResult {
if h.ToolName == "bash" {
return &kit.BeforeToolCallResult{Block: true, Reason: "bash disabled"}
}
return nil
})
AfterToolResult — modify tool output
host.OnAfterToolResult(kit.HookPriorityNormal, func(h kit.AfterToolResultHook) *kit.AfterToolResultResult {
if h.ToolName == "read" {
filtered := redactSecrets(h.Result)
return &kit.AfterToolResultResult{Result: &filtered}
}
return nil
})
BeforeTurn — modify prompt, inject messages
host.OnBeforeTurn(kit.HookPriorityNormal, func(h kit.BeforeTurnHook) *kit.BeforeTurnResult {
newPrompt := h.Prompt + "\nAlways respond in JSON."
return &kit.BeforeTurnResult{Prompt: &newPrompt}
})
AfterTurn — observation only
host.OnAfterTurn(kit.HookPriorityNormal, func(h kit.AfterTurnHook) {
log.Printf("Turn completed: %d chars", len(h.Response))
})
PrepareStep — intercept/replace messages before each LLM call
host.OnPrepareStep(kit.HookPriorityNormal, func(h kit.PrepareStepHook) *kit.PrepareStepResult {
modified := filterSensitiveMessages(h.Messages)
return &kit.PrepareStepResult{Messages: modified}
})
PrepareStep fires before every LLM API call within a turn (including tool-call loop iterations). Unlike ContextPrepare (which operates on the full context window once per turn), PrepareStep runs per-step and sees the messages that include the latest tool results.
ContextPrepare — filter/inject context window
host.OnContextPrepare(kit.HookPriorityNormal, func(h kit.ContextPrepareHook) *kit.ContextPrepareResult {
return &kit.ContextPrepareResult{Messages: filteredMessages}
})
BeforeCompact — cancel or customize compaction
host.OnBeforeCompact(kit.HookPriorityNormal, func(h kit.BeforeCompactHook) *kit.BeforeCompactResult {
if h.IsAutomatic && h.UsagePercent < 0.9 {
return &kit.BeforeCompactResult{Cancel: true, Reason: "not yet"}
}
return nil
})
Hook priorities
kit.HookPriorityHigh = 0
kit.HookPriorityNormal = 50
kit.HookPriorityLow = 100
Lower values run first. Within the same priority, registration order applies. First non-nil result wins.
Tools
Creating custom tools
Use kit.NewTool to create custom tools. The JSON schema is auto-generated from the input struct — no external dependencies required:
type WeatherInput struct {
City string `json:"city" description:"City name, e.g. 'San Francisco'"`
}
weatherTool := kit.NewTool("get_weather", "Get current weather for a city",
func(ctx context.Context, input WeatherInput) (kit.ToolOutput, error) {
return kit.TextResult("72°F, sunny in " + input.City), nil
},
)
host, _ := kit.New(ctx, &kit.Options{
ExtraTools: []kit.Tool{weatherTool},
})
Struct tags control the generated schema:
| Tag | Purpose | Example |
|---|
json:"name" | Parameter name | json:"city" |
description:"..." | Description shown to the LLM | description:"City name" |
enum:"a,b,c" | Restrict valid values | enum:"json,text,csv" |
omitempty | Marks parameter as optional | json:"limit,omitempty" |
Return helpers:
| Function | Description |
|---|
kit.TextResult(content) | Successful text result |
kit.ErrorResult(content) | Error result (LLM sees it as a tool error) |
kit.ImageResult(content, data, mediaType) | Image result with binary data (e.g. "image/png") |
kit.MediaResult(content, data, mediaType) | Non-image media result (e.g. "audio/mpeg") |
ToolOutput fields (for advanced use):
kit.ToolOutput{
Content: "result text",
IsError: false,
Data: pngBytes,
MediaType: "image/png",
Metadata: map[string]any{},
}
Parallel tools — mark as safe for concurrent execution:
searchTool := kit.NewParallelTool("search", "Search the web",
func(ctx context.Context, input SearchInput) (kit.ToolOutput, error) {
return kit.TextResult("results..."), nil
},
)
Tool call ID — available in context for logging/tracing:
tool := kit.NewTool("my_tool", "...",
func(ctx context.Context, input MyInput) (kit.ToolOutput, error) {
callID := kit.ToolCallIDFromContext(ctx)
log.Printf("[%s] my_tool called", callID)
return kit.TextResult("ok"), nil
},
)
Built-in tool constructors
kit.NewReadTool(opts...)
kit.NewWriteTool(opts...)
kit.NewEditTool(opts...)
kit.NewBashTool(opts...)
kit.NewGrepTool(opts...)
kit.NewFindTool(opts...)
kit.NewLsTool(opts...)
Tool bundles
kit.AllTools(opts...)
kit.CodingTools(opts...)
kit.ReadOnlyTools(opts...)
kit.SubagentTools(opts...)
Tool options
kit.WithWorkDir("/path/to/dir")
Using tools in Options
host, _ := kit.New(ctx, &kit.Options{
Tools: []kit.Tool{kit.NewBashTool()},
})
host, _ := kit.New(ctx, &kit.Options{
ExtraTools: []kit.Tool{myCustomTool},
})
Querying tools at runtime
names := host.GetToolNames()
tools := host.GetTools()
mcpCount := host.GetMCPToolCount()
extCount := host.GetExtensionToolCount()
ready := host.MCPToolsReady()
Session Management
Sessions automatically persist as JSONL tree files. No explicit save needed.
Session modes (via Options)
| Mode | Options | Behavior |
|---|
| Default | {} | New session file for cwd |
| Specific file | {SessionPath: "path.jsonl"} | Open existing session |
| Continue | {Continue: true} | Resume most recent session for cwd |
| Ephemeral | {NoSession: true} | In-memory only, no disk persistence |
| Custom dir | {SessionDir: "/path"} | Base directory for session discovery |
Instance methods
host.GetSessionPath()
host.GetSessionID()
host.ClearSession()
host.Branch("entry-id")
host.SetSessionName("my session")
msgs := host.GetSessionMessages()
msgs := host.GetStructuredMessages()
Package-level session operations (no Kit instance needed)
sessions, _ := kit.ListSessions("/path/to/project")
sessions, _ := kit.ListAllSessions()
kit.DeleteSession("/path/to/session.jsonl")
tm, _ := kit.OpenTreeSession("/path/to/session.jsonl")
Custom Session Manager (Advanced)
You can provide a custom session manager to store conversation history in your own backend (database, cloud storage, etc.) instead of the default JSONL files.
type MyDatabaseSessionManager struct {
db *sql.DB
}
func (s *MyDatabaseSessionManager) AppendMessage(msg kit.LLMMessage) (string, error) {
}
func (s *MyDatabaseSessionManager) GetMessages() []kit.LLMMessage {
}
host, _ := kit.New(ctx, &kit.Options{
SessionManager: myCustomSession,
Model: "anthropic/claude-sonnet-latest",
})
SessionManager Interface:
type SessionManager interface {
AppendMessage(msg kit.LLMMessage) (entryID string, err error)
GetMessages() []kit.LLMMessage
BuildContext() (messages []kit.LLMMessage, provider string, modelID string)
Branch(entryID string) error
GetCurrentBranch() []kit.BranchEntry
GetChildren(parentID string) []string
GetEntry(entryID string) *kit.BranchEntry
GetSessionID() string
GetSessionName() string
SetSessionName(name string) error
GetCreatedAt() time.Time
IsPersisted() bool
AppendCompaction(summary string, firstKeptEntryID string,
tokensBefore, tokensAfter int, messagesRemoved int, readFiles, modifiedFiles []string) (string, error)
GetLastCompaction() *kit.CompactionEntry
AppendExtensionData(extType, data string) (string, error)
GetExtensionData(extType string) []kit.ExtensionDataEntry
AppendModelChange(provider, modelID string) (string, error)
GetContextEntryIDs() []string
Close() error
}
Use Cases:
- PocketBase integration: Store sessions as PocketBase records
- Cloud storage: Persist sessions to S3, GCS, or Azure Blob
- Multi-user apps: Store sessions per user in a database
- Custom retention: Implement your own session cleanup policies
Note: When using a custom SessionManager, the following Options are ignored:
SessionPath - your manager handles its own storage
Continue - your manager handles session selection
NoSession - use an in-memory implementation instead
Model Management
At creation time
host, _ := kit.New(ctx, &kit.Options{
Model: "openai/gpt-4o",
})
At runtime
err := host.SetModel(ctx, "anthropic/claude-sonnet-4-5-20250929")
modelStr := host.GetModelString()
info := host.GetModelInfo()
isReasoning := host.IsReasoningModel()
level := host.GetThinkingLevel()
err = host.SetThinkingLevel(ctx, "medium")
Model registry
models := host.GetAvailableModels()
providers := kit.GetSupportedProviders()
providers := kit.GetLLMProviders()
models, _ := kit.GetModelsForProvider("anthropic")
info := kit.LookupModel("anthropic", "claude-sonnet-4-5-20250929")
info := kit.GetProviderInfo("openai")
err := kit.ValidateEnvironment("anthropic", "")
suggestions := kit.SuggestModels("anthropic", "claudee")
kit.RefreshModelRegistry()
Model string format
Always "provider/model": "anthropic/claude-sonnet-4-5-20250929", "openai/gpt-4o", "ollama/qwen3:8b".
provider, modelID, err := kit.ParseModelString("anthropic/claude-sonnet-4-5-20250929")
Per-model system prompts
Models can have per-model system prompts configured via modelSettings or customModels in .kit.yml. When the user hasn't explicitly set a system prompt (via --system-prompt, config, or Options.SystemPrompt), the per-model prompt is used as the base and composed with AGENTS.md context and skills.
On SetModel(), if the new model has a per-model system prompt and no custom global prompt was set, the per-model prompt automatically replaces the previous one.
Per-model generation parameters
Models can define default generation parameters (temperature, top_p, top_k, frequency_penalty, presence_penalty) via modelSettings or customModels params in .kit.yml. These defaults apply when the user hasn't explicitly set the parameter. Explicit CLI flags or config values always take priority.
Dynamic MCP Server Management
Add, remove, and inspect MCP servers at runtime without restarting Kit:
n, err := host.AddMCPServer(ctx, "github", kit.MCPServerConfig{
Command: []string{"npx", "-y", "@modelcontextprotocol/server-github"},
Environment: map[string]string{"GITHUB_TOKEN": os.Getenv("GITHUB_TOKEN")},
})
fmt.Printf("Loaded %d tools from github server\n", n)
err = host.RemoveMCPServer("github")
servers := host.ListMCPServers()
for _, s := range servers {
fmt.Printf("Server %s: %d tools\n", s.Name, s.ToolCount)
}
AddMCPServer is safe to call while the agent is idle. If a turn is in progress, new tools are visible starting from the next LLM step. Tool names are prefixed with the server name (e.g. "github__create_issue").
In-Process MCP Servers
Register mcp-go servers that run in the same process — no subprocess spawning,
no network I/O:
import (
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
mcpSrv := server.NewMCPServer("my-tools", "1.0.0",
server.WithToolCapabilities(true),
)
mcpSrv.AddTool(mcp.NewTool("search_docs",
mcp.WithDescription("Search documentation"),
mcp.WithString("query", mcp.Required()),
), searchHandler)
host, _ := kit.New(ctx, &kit.Options{
InProcessMCPServers: map[string]*kit.MCPServer{
"docs": mcpSrv,
},
})
n, err := host.AddInProcessMCPServer(ctx, "docs", mcpSrv)
Kit does not own the server lifecycle — the caller handles cleanup. Tools are prefixed as usual (e.g. "docs__search_docs").
MCP Prompts
Query and expand prompts defined by connected MCP servers:
prompts := host.ListMCPPrompts()
for _, p := range prompts {
fmt.Printf("%s/%s: %s\n", p.ServerName, p.Name, p.Description)
for _, arg := range p.Arguments {
fmt.Printf(" arg: %s (required: %v)\n", arg.Name, arg.Required)
}
}
result, err := host.GetMCPPrompt(ctx, "myserver", "code-review", map[string]string{
"language": "go",
"style": "thorough",
})
for _, msg := range result.Messages {
fmt.Printf("[%s] %s\n", msg.Role, msg.Content)
}
MCP Resources
Read and subscribe to resources exposed by MCP servers:
resources := host.ListMCPResources()
for _, r := range resources {
fmt.Printf("%s: %s (%s)\n", r.URI, r.Name, r.MIMEType)
}
content, err := host.ReadMCPResource(ctx, "myserver", "file:///path/to/file")
if content.IsBlob {
} else {
}
err = host.SubscribeMCPResource(ctx, "myserver", "file:///path/to/file")
err = host.UnsubscribeMCPResource(ctx, "myserver", "file:///path/to/file")
MCP OAuth Authorization
When a remote MCP server requires OAuth, Kit runs the full authorization flow
(dynamic client registration → PKCE → user consent → token exchange → token
persistence) but delegates the user-facing step — displaying the
authorization URL and receiving the callback — to an MCPAuthHandler.
The SDK ships three building blocks:
| Building block | When to use |
|---|
No handler (Options.MCPAuthHandler = nil) | Default. OAuth is disabled; 401s from remote MCP servers surface as errors. Correct for library, daemon, and web-app embedders that don't want side effects. |
kit.NewCLIMCPAuthHandler() | CLI/TUI apps. Opens the system browser, prints status to stderr (or via NotifyFunc), runs a localhost callback server. This is what the kit binary uses. |
kit.NewDefaultMCPAuthHandler() + OnAuthURL | Custom UX. Get the transport mechanics (port reservation + callback server) from the SDK; wire your own presentation in the OnAuthURL(serverName, authURL) closure. |
Implement kit.MCPAuthHandler directly | Full control. No localhost binding — e.g. return the URL from an HTTP endpoint and have the consumer POST the callback URL back. |
CLI-style embedder (browser + stderr):
authHandler, err := kit.NewCLIMCPAuthHandler()
if err != nil {
log.Fatal(err)
}
defer authHandler.Close()
host, _ := kit.New(ctx, &kit.Options{
MCPAuthHandler: authHandler,
})
Custom UX embedder (TUI modal, QR code, web redirect, etc.):
authHandler, _ := kit.NewDefaultMCPAuthHandler()
authHandler.OnAuthURL = func(serverName, authURL string) {
myUI.ShowAuthPrompt(serverName, authURL)
}
defer authHandler.Close()
host, _ := kit.New(ctx, &kit.Options{
MCPAuthHandler: authHandler,
})
Important: DefaultMCPAuthHandler with no OnAuthURL set will silently
drop the authorization URL and block until the 2-minute callback timeout
fires. Always set OnAuthURL, or use a higher-level wrapper like
CLIMCPAuthHandler.
MCP OAuth Token Storage
Once authorization succeeds, the resulting access/refresh tokens are persisted
by an MCPTokenStore. By default tokens are written to
$XDG_CONFIG_HOME/.kit/mcp_tokens.json (fallback ~/.config/.kit/mcp_tokens.json),
keyed by server URL, with 0600 file permissions.
Provide a custom store for encrypted storage, database persistence, or
in-memory-only flows:
host, _ := kit.New(ctx, &kit.Options{
MCPTokenStoreFactory: func(serverURL string) (kit.MCPTokenStore, error) {
return &MyDatabaseTokenStore{serverURL: serverURL}, nil
},
})
The MCPTokenStore interface requires GetToken/SetToken/DeleteToken methods. Return kit.ErrMCPNoToken from GetToken when no token is stored.
Context & Compaction
tokens := host.EstimateContextTokens()
shouldCompact := host.ShouldCompact()
stats := host.GetContextStats()
result, err := host.Compact(ctx, nil, "")
host, _ := kit.New(ctx, &kit.Options{
AutoCompact: true,
CompactionOptions: &kit.CompactionOptions{
ReserveTokens: 16384,
KeepRecentTokens: 4096,
ContextWindow: 200000,
},
})
In-Process Subagents
Spawn child Kit instances without subprocess overhead:
result, err := host.Subagent(ctx, kit.SubagentConfig{
Prompt: "Analyze the test files and summarize coverage",
Model: "anthropic/claude-haiku-3-5-20241022",
SystemPrompt: "You are a test analysis expert.",
Tools: nil,
NoSession: true,
Timeout: 2 * time.Minute,
OnEvent: func(e kit.Event) {
if chunk, ok := e.(kit.MessageUpdateEvent); ok {
fmt.Print(chunk.Chunk)
}
},
})
Subscribing to subagent events from parent
host.OnToolCall(func(e kit.ToolCallEvent) {
if e.ToolName == "subagent" {
host.SubscribeSubagent(e.ToolCallID, func(child kit.Event) {
})
}
})
Extension API
The Extensions() method returns an ExtensionAPI interface that groups all extension-related functionality. This is the primary way to interact with extension state from the SDK.
extAPI := host.Extensions()
if extAPI.HasExtensions() {
extAPI.SetContext(extensions.Context{...})
ctx := extAPI.GetContext()
extAPI.UpdateContextModel("anthropic/claude-sonnet-4-5-20250929")
extAPI.SetWidget(extensions.WidgetConfig{...})
extAPI.RemoveWidget("widget-id")
extAPI.SetHeader(extensions.HeaderFooterConfig{...})
extAPI.SetFooter(extensions.HeaderFooterConfig{...})
extAPI.SetStatus(extensions.StatusBarEntry{...})
extAPI.RemoveStatus("key")
extAPI.SetOption("name", "value")
val := extAPI.GetOption("name")
tools := extAPI.GetToolInfos()
extAPI.SetActiveTools([]string{"bash", "read"})
extAPI.EmitSessionStart()
extAPI.EmitModelChange("new/model", "old/model", "extension")
extAPI.EmitCustomEvent("my-event", "data")
cmds := extAPI.Commands()
err := extAPI.Reload()
}
All methods are no-ops when extensions are disabled (nil runner), so callers don't need nil checks.
Authentication
cm, _ := kit.NewCredentialManager()
hasKey := kit.HasAnthropicCredentials()
apiKey := kit.GetAnthropicAPIKey()
Skills
skill, _ := kit.LoadSkill("/path/to/SKILL.md")
skills, _ := kit.LoadSkillsFromDir("/path/to/skills")
skills, _ := kit.LoadSkills("/path/to/project")
pb := kit.NewPromptBuilder("You are an assistant")
pb.WithSkills(skills)
pb.WithSection("", "Extra context here")
systemPrompt := pb.Build()
Re-exported Types
The SDK re-exports internal types so you don't need direct internal imports:
kit.Message, kit.MessageRole, kit.ContentPart
kit.TextContent, kit.ReasoningContent, kit.ToolCall, kit.ToolResult, kit.Finish
kit.RoleUser, kit.RoleAssistant, kit.RoleTool, kit.RoleSystem
kit.SessionInfo, kit.TreeManager, kit.SessionHeader, kit.MessageEntry
kit.Config, kit.MCPServerConfig
kit.ProviderConfig, kit.ProviderResult, kit.ModelInfo, kit.ModelCost, kit.ModelLimit
kit.LLMMessage
kit.LLMMessagePart
kit.LLMMessageRole
kit.LLMUsage
kit.LLMResponse
kit.LLMFilePart
kit.LLMTextPart
kit.LLMReasoningPart
kit.LLMToolCall
kit.LLMToolResponse
kit.LLMToolCallPart
kit.LLMToolResultPart
kit.LLMToolResultOutputContent
kit.LLMToolResultOutputContentText
kit.LLMToolResultOutputContentError
kit.LLMToolResultOutputContentMedia
kit.LLMToolResultContentType
kit.LLMToolInfo
kit.LLMProviderOptions
kit.LLMProviderMetadata
kit.LLMPrompt
kit.LLMFinishReason
kit.CompactionResult, kit.CompactionOptions
kit.MCPAuthHandler
kit.DefaultMCPAuthHandler
kit.CLIMCPAuthHandler
kit.NewDefaultMCPAuthHandler()
kit.NewDefaultMCPAuthHandlerWithPort()
kit.NewCLIMCPAuthHandler()
kit.MCPTokenStore
kit.MCPToken
kit.MCPTokenStoreFactory
kit.ErrMCPNoToken
kit.MCPServer
kit.MCPServerStatus
kit.MCPPrompt
kit.MCPPromptArgument
kit.MCPPromptMessage
kit.MCPPromptResult
kit.MCPResource
kit.MCPResourceContent
msgs := kit.ConvertToLLMMessages(&msg)
msg := kit.ConvertFromLLMMessage(lMsg)
Common Patterns
Pattern: Scripting / CLI pipe
Minimal program for automation — stdout-only output:
host, _ := kit.New(ctx, &kit.Options{Quiet: true})
defer func() { _ = host.Close() }()
response, _ := host.Prompt(ctx, os.Args[1])
fmt.Println(response)
Pattern: Long-running autonomous agent
Daemon that performs repeated independent tasks:
host, _ := kit.New(ctx, &kit.Options{
SystemPrompt: taskPrompt,
Tools: []kit.Tool{kit.NewBashTool()},
NoSession: true,
Quiet: true,
})
defer func() { _ = host.Close() }()
ticker := time.NewTicker(30 * time.Minute)
for {
select {
case <-ticker.C:
host.ClearSession()
host.Prompt(ctx, "Perform the monitoring task")
case <-ctx.Done():
return
}
}
Pattern: Streaming output to terminal
host.OnMessageUpdate(func(e kit.MessageUpdateEvent) {
fmt.Print(e.Chunk)
})
response, _ := host.Prompt(ctx, "Write a poem")
Pattern: Multi-turn conversation with memory
host.Prompt(ctx, "My name is Alice")
response, _ := host.Prompt(ctx, "What's my name?")
fmt.Printf("Session: %s\n", host.GetSessionPath())
Pattern: Tool execution monitoring
host.OnToolCall(func(e kit.ToolCallEvent) {
fmt.Printf("[%s] %s(%s)\n", e.ToolKind, e.ToolName, e.ToolArgs)
})
host.OnToolResult(func(e kit.ToolResultEvent) {
status := "✓"
if e.IsError { status = "✗" }
fmt.Printf("[%s] %s %s\n", e.ToolKind, status, e.ToolName)
})
Pattern: Guard rails with hooks
host.OnBeforeToolCall(kit.HookPriorityHigh, func(h kit.BeforeToolCallHook) *kit.BeforeToolCallResult {
if h.ToolName == "bash" && strings.Contains(h.ToolArgs, "rm -rf") {
return &kit.BeforeToolCallResult{Block: true, Reason: "dangerous command"}
}
return nil
})
host.OnBeforeTurn(kit.HookPriorityNormal, func(h kit.BeforeTurnHook) *kit.BeforeTurnResult {
context := "Current user: admin\nEnvironment: production"
return &kit.BeforeTurnResult{InjectText: &context}
})
Pattern: Parallel subagents
var wg sync.WaitGroup
results := make([]*kit.SubagentResult, 3)
tasks := []string{"Analyze auth module", "Analyze database layer", "Analyze API routes"}
for i, task := range tasks {
wg.Add(1)
go func(idx int, t string) {
defer wg.Done()
results[idx], _ = host.Subagent(ctx, kit.SubagentConfig{
Prompt: t,
NoSession: true,
Timeout: 3 * time.Minute,
})
}(i, task)
}
wg.Wait()
Pattern: Read-only analysis agent
host, _ := kit.New(ctx, &kit.Options{
SystemPrompt: "You are a code reviewer. Only read and analyze, never modify files.",
Tools: kit.ReadOnlyTools(),
})
Configuration
The SDK loads config identically to the CLI:
- Explicit
ConfigFile in Options (highest priority)
.kit.yml in current directory
~/.kit.yml in home directory
- Environment variables with
KIT_ prefix (KIT_MODEL, etc.)
- Provider-specific env vars (
ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
Config files support ${ENV_VAR} expansion.
kit.InitConfig("/path/to/config.yml", false)
kit.LoadConfigWithEnvSubstitution("/path/to/config.yml")
Key Files for Reference