Documentation
¶
Overview ¶
Schema cache: a best-effort, on-disk record of what tools/list (plus serverInfo) returned for a given server the last time Kram actually connected to it. Keyed by server name + a fingerprint of the connection config that produced it, so a config edit invalidates the old entry automatically rather than serving stale tools under a changed identity.
Single file (kramhome/mcp-cache.json), not one file per server: with the number of MCP servers a real workspace configures (single digits, almost always), one small JSON file is simpler to read, write, and reason about atomically than a directory of them — no directory-listing, no per-file locking, no orphaned files left behind when a server is removed from mcp.json. The whole file is read, mutated in memory, and rewritten on every save, which is fine at this scale.
What this delivers today: after every real tools/list (initial connect, a successful reconnect, or a tools/list_changed-triggered refresh), the result is written here. That makes the cache useful for diagnostics (an operator or a future tool can answer "what did this server last advertise, and when" without starting it) and gives a cheap fingerprint check that a config change actually altered the connection identity.
What this does NOT do yet: nothing in Connect/ConnectAll consults the cache to decide whether it's safe to skip starting a stdio process for pure discovery — ConnectAll's contract (always dial every enabled server at startup, logging and skipping failures) is left exactly as it was. cachedTools below is the seam a future "trust the cache, connect lazily" mode would call; it's unused by production code today except by its own tests, which is deliberate infrastructure-ahead-of-need rather than a half-wired feature.
Package mcp is a from-scratch Model Context Protocol client — the way Kram reaches third-party tool servers (filesystem, GitHub, databases, whatever someone has published) instead of only the tools compiled into its own binary. No SDK dependency: MCP is JSON-RPC 2.0 over stdio or HTTP, and hand-rolling it keeps Kram's "pure-Go, static binary, no surprise dependencies" property.
Protocol era: this implements the *stateful* lifecycle (`initialize` → `notifications/initialized` → `tools/list`/`tools/call`) used by revisions through 2025-11-25. The 2026-07-28 revision removes the handshake entirely in favor of stateless per-request metadata; that's deliberately deferred, because essentially every MCP server actually deployed today speaks the older lifecycle. See handshake() for where a modern-era probe would slot in.
Index ¶
- func LoadConfig(workspace string) map[string]ServerConfig
- type Client
- func (c *Client) CallTool(ctx context.Context, name string, args json.RawMessage) (string, error)
- func (c *Client) Close() error
- func (c *Client) Done() <-chan struct{}
- func (c *Client) GetPrompt(ctx context.Context, name string, arguments map[string]string) (string, error)
- func (c *Client) ListPrompts(ctx context.Context) ([]Prompt, error)
- func (c *Client) ListResources(ctx context.Context) ([]Resource, error)
- func (c *Client) ReadResource(ctx context.Context, uri string) (string, error)
- func (c *Client) ServerInfo() string
- func (c *Client) Tools() []Tool
- type Manager
- type Prompt
- type PromptArgument
- type Resource
- type ServerConfig
- type Tool
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func LoadConfig ¶
func LoadConfig(workspace string) map[string]ServerConfig
LoadConfig merges the global server list (kramhome/mcp.json) with the project's own (<workspace>/.kram/mcp.json). A project entry wins on name collision, which is what lets a repo pin a different version of a server than the user's global default.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a connected MCP server: one transport, one pending-request table, and the tools the server advertised.
func Connect ¶
Connect starts (or dials) one configured server, performs the handshake, and lists its tools. A server that fails any of that is returned as an error and simply won't contribute tools — one broken entry in a config must never stop the daemon from starting.
func (*Client) CallTool ¶
CallTool invokes one tool and flattens the result into plain text, which is what Kram's tool protocol carries. A tool-level failure (isError) comes back as text rather than a Go error on purpose: the agent loop feeds it straight back to the model, which can then read what went wrong and correct itself — the same contract Kram's built-in tools follow.
func (*Client) Done ¶
func (c *Client) Done() <-chan struct{}
Done closes once this client's transport has gone away — dispatch() noticing Recv() close, whether because the server process died, the connection dropped, or Close() was called deliberately. A Manager uses this to know when to start reconnecting a specific server.
func (*Client) GetPrompt ¶
func (c *Client) GetPrompt(ctx context.Context, name string, arguments map[string]string) (string, error)
GetPrompt resolves one named prompt template with the given arguments and flattens its messages to text — Kram surfaces this as a tool result for the model to read and act on, not as literal conversation messages injected into history.
func (*Client) ListPrompts ¶
ListPrompts fetches every prompt template this server currently exposes, same on-demand-not-cached reasoning as ListResources.
func (*Client) ListResources ¶
ListResources fetches every resource this server currently exposes. Unlike Tools (cached at connect time), this is fetched fresh on every call — a server's resource list can be large or change, and Kram has no reason to hold a stale copy of it in memory between calls.
func (*Client) ReadResource ¶
ReadResource fetches one resource's content by URI and flattens it to text, same convention CallTool uses — a binary resource (Blob) is reported as a placeholder rather than decoded, since Kram's tool results are text-only end to end.
func (*Client) ServerInfo ¶
ServerInfo is the server's self-reported name and version, for logs.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns every connected server for one daemon, and supervises each one for the rest of the daemon's life: if a server's transport dies, the Manager reconnects it with backoff rather than leaving it dead until the next restart.
func ConnectAll ¶
ConnectAll dials every enabled server in cfg, in name order so startup logs are stable. Failures are logged and skipped rather than fatal: a broken or uninstalled MCP server should cost you that server's tools, nothing else. Every server that does connect is then supervised for the life of ctx — if its transport later dies, the Manager reconnects it with backoff (see superviseServer) instead of leaving it dead.
func (*Manager) Clients ¶
Clients returns a snapshot of every currently connected server by name. A copy, not the live map: reconnect goroutines mutate the Manager's internal map concurrently, so handing out the map itself would be a data race for any caller that ranges over it (RegisterMCP, the resources/prompts tools) while a reconnect is in flight.
func (*Manager) Close ¶
func (m *Manager) Close()
Close stops supervising every server and shuts every currently connected one down. Cancels the Manager's own context first — which unblocks any supervisor goroutine waiting on a client, and any backoff sleep in progress — and waits for all of them to actually exit before touching a single client, so a client is never closed out from under a supervisor that might otherwise mistake the deliberate shutdown for a crash and try to reconnect into a Manager that's going away.
type Prompt ¶
type Prompt struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Arguments []PromptArgument `json:"arguments,omitempty"`
}
Prompt is a server-defined, parameterized prompt template.
type PromptArgument ¶
type Resource ¶
type Resource struct {
URI string `json:"uri"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType,omitempty"`
}
Resource is one piece of server-exposed readable data, identified by URI — a file, a database row, whatever the server wants to expose.
type ServerConfig ¶
type ServerConfig struct {
Type string `json:"type,omitempty"` // "stdio" (default when command is set) or "http"
Command string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
URL string `json:"url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Enabled *bool `json:"enabled,omitempty"` // nil means enabled
}
ServerConfig is one entry in an mcpServers map. The shape deliberately matches the de-facto convention Claude Desktop, Claude Code and opencode all use (command/args/env for stdio, url/headers for HTTP), so an existing mcp.json can be pointed at Kram without rewriting it.
type Tool ¶
type Tool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema json.RawMessage `json:"inputSchema"`
}
Tool is one tool a server exposes. InputSchema is passed through to the LLM provider untouched — it's already JSON Schema, which is exactly what a tool definition needs.