mcp

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 18 Imported by: 0

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

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

func Connect(ctx context.Context, name string, cfg ServerConfig) (*Client, error)

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

func (c *Client) CallTool(ctx context.Context, name string, args json.RawMessage) (string, error)

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) Close

func (c *Client) Close() error

Close shuts the server down.

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

func (c *Client) ListPrompts(ctx context.Context) ([]Prompt, error)

ListPrompts fetches every prompt template this server currently exposes, same on-demand-not-cached reasoning as ListResources.

func (*Client) ListResources

func (c *Client) ListResources(ctx context.Context) ([]Resource, error)

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

func (c *Client) ReadResource(ctx context.Context, uri string) (string, error)

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

func (c *Client) ServerInfo() string

ServerInfo is the server's self-reported name and version, for logs.

func (*Client) Tools

func (c *Client) Tools() []Tool

Tools returns the server's most recently known tool list — what it advertised at connect time, or what a later tools/list_changed notification refreshed it to, whichever is newer. Safe to call concurrently with a refresh in progress.

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

func ConnectAll(ctx context.Context, cfg map[string]ServerConfig, logger *slog.Logger) *Manager

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

func (m *Manager) Clients() map[string]*Client

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 PromptArgument struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Required    bool   `json:"required,omitempty"`
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL