mcpmgr

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrUnauthorized = errors.New("mcp: unauthorized")

ErrUnauthorized is what errors.Is matches for an *UnauthorizedError: the server answered 401 and a refresh, if there was a hook to try one, did not help. It means "log in again", not "the server is broken".

Functions

func RenderSecret

func RenderSecret(format string, kv map[string]string) ([]byte, error)

RenderSecret serialises a flat key/value secret map into one of the supported on-disk formats. The rendered bytes are written to a per-spawn 0600 temp file whose path replaces the {{secret_file}} placeholder in an MCP server's Args/Env (see manager.spawn).

Supported formats:

  • "env" — KEY=VALUE lines (gmcpail consumes this)
  • "properties" — key=value lines (java .properties variant)
  • "json" — a JSON object

There was a "yaml" format. It was removed with gopkg.in/yaml.v3, which was 11k LOC carried for that one branch and had no users — every config in the field renders env. A server that genuinely needs YAML can take "json" instead: YAML 1.2 is a superset of JSON, so a JSON object IS a YAML document, and any conforming parser reads it unchanged.

Keys are sorted so output is deterministic.

func ThreadServerID

func ThreadServerID(integrationID, threadID string) string

ThreadServerID is the convention for cfg.ID on thread-scoped servers — composite so the scheduler's resolveTool can recover the (integration, thread) pair from a single ServerID field and so tool names stay collision-free across threads.

Types

type MCPConfig

type MCPConfig struct {
	ID      string   `json:"id"`
	Name    string   `json:"name"`
	Command string   `json:"command"`
	Args    []string `json:"args"`
	Env     []string `json:"env"`
	Timeout int      `json:"timeout"` // seconds, default 30
	Scope   string   `json:"scope"`

	// URL selects the Streamable HTTP transport (http.go) instead of
	// spawning Command. Headers go on every request — Authorization: Bearer
	// <token> for a server that takes a static token.
	URL     string            `json:"url,omitempty"`
	Headers map[string]string `json:"headers,omitempty"`

	// Authorization, when set, is called before every HTTP request and its
	// return value is sent as the Authorization header ("Bearer <token>"),
	// replacing any in Headers; "" sends none. It is called often and from
	// several goroutines: cache the token, and return quickly. An error
	// fails the request. Not serialized — a host sets it in code.
	Authorization func(ctx context.Context) (string, error) `json:"-"`
	// OnUnauthorized, when set, runs when a request gets HTTP 401, with the
	// response's WWW-Authenticate header (resource_metadata in it starts
	// RFC 9728 discovery — see mcpauth). It should refresh or obtain the
	// token that Authorization returns; the request is then retried once.
	// A second 401, or no hook, fails with an *UnauthorizedError
	// (errors.Is ErrUnauthorized). Concurrent 401s run it one at a time,
	// and not at all when Authorization already returns a value different
	// from the one that failed. Not serialized.
	OnUnauthorized func(ctx context.Context, wwwAuthenticate string) error `json:"-"`
	// OnStderr, when set, gets each line a spawned server writes on stderr,
	// as it writes it: from the spawn on, through the handshake and for the
	// life of the process (ServerStderr only has the tail of a server that
	// finished its handshake). It runs on the stderr reader's goroutine, so
	// it hands off and returns; blocking it stalls the server's stderr pipe.
	// Blank lines are skipped and long ones cut, as in the tail.
	OnStderr func(line string) `json:"-"`

	// SecretRef names a row in the secrets table whose plaintext is a
	// JSON object (a flat KV map). When set, spawn() resolves it,
	// renders it to SecretFormat, writes it to a per-server 0600 temp
	// file, and substitutes the literal token {{secret_file}} in every
	// Args and Env entry with that file's path. No secret value ever
	// lands in Args/Env directly — only the file path. Carried in the
	// integration's JSONB config (type=mcp); no schema column.
	SecretRef string `json:"secret_ref"`
	// SecretFormat selects the on-disk encoding of the rendered secret
	// file: one of env|properties|json. Only consulted when SecretRef is
	// set. A server wanting YAML takes json — YAML 1.2 is a superset of
	// JSON.
	SecretFormat string `json:"secret_format"`
}

MCPConfig holds the configuration for an MCP server connection.

Scope:

  • "" — backwards-compat default, treated as "project"
  • "project" — one shared instance per integration, spawned at daemon boot. Use when the MCP server is workspace- agnostic OR indexes the project's canonical repo_path.
  • "thread" — one instance per (integration, thread) pair, spawned lazily when the thread's first dev session activates. The Args list may contain "{{worktree_root}}" placeholders that the scheduler substitutes with the thread's worktree directory at spawn time. Tears down on thread close. Use for per-workspace indexers (poly-lsp-mcp and friends) where each thread has its own worktree.

type MCPServer

type MCPServer struct {
	Config MCPConfig

	Tools []MCPTool
	// Capabilities is what the server declared in the handshake: "tools",
	// "resources", "prompts", … — a client asks only for what is there.
	Capabilities map[string]any
	// Info is what the server said about itself in the handshake.
	Info ServerInfo
	// contains filtered or unexported fields
}

MCPServer wraps a connected MCP client.

type MCPTool

type MCPTool struct {
	Name        string                 `json:"name"`
	Description string                 `json:"description"`
	InputSchema map[string]interface{} `json:"inputSchema"`
	ServerID    string                 `json:"server_id"`
}

MCPTool represents a tool discovered from an MCP server.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager manages MCP server connections, both project-scoped (one shared instance per integration) and thread-scoped (one instance per (integration, thread) pair). Thread-scoped servers are what makes a per-thread editor MCP work: each thread's poly-lsp-mcp indexes only that thread's worktree, so file operations stay single-threaded within a thread (the dev-lane lock already serializes Turns) while threads make progress independently.

Key conventions:

  • servers[cfg.ID] — project-scoped, started at boot
  • threadServers[integID][threadID] — thread-scoped, started lazily when the first dev session for the thread activates

func NewManager

func NewManager() *Manager

func NewManagerWithSecrets

func NewManagerWithSecrets(resolver SecretResolver) *Manager

NewManagerWithSecrets builds a Manager wired to a SecretResolver. MCP configs that carry a non-empty SecretRef will have the resolved secret rendered to a 0600 temp file and substituted into Args/Env via the {{secret_file}} placeholder at spawn time.

func (*Manager) CallTool

func (m *Manager) CallTool(ctx context.Context, serverID string, toolName string, args map[string]interface{}) (string, error)

CallTool invokes a tool on the appropriate MCP server. ServerID may be a project-scoped id (one of m.servers' keys) OR a thread-scoped id produced by ThreadServerID(integID, threadID). Routes to whichever map holds it.

func (*Manager) Capabilities added in v0.7.0

func (m *Manager) Capabilities(serverID string) map[string]any

Capabilities returns what a connected server declared in the handshake, or nil when it is not connected.

func (*Manager) Close

func (m *Manager) Close()

Close disconnects all MCP servers (project + thread scope).

func (*Manager) GetPrompt added in v0.7.0

func (m *Manager) GetPrompt(ctx context.Context, serverID, name string, args map[string]string) (string, []PromptMessage, error)

GetPrompt renders one prompt with its arguments (prompts/get): the prompt's description and its messages.

func (*Manager) GetThreadTools

func (m *Manager) GetThreadTools(threadID string) []MCPTool

GetThreadTools returns the tool set the scheduler should expose to developers on threadID: project-scoped tools (unchanged behaviour) PLUS any thread-scoped tools bound to this thread. Callers that don't care about thread scoping can continue using GetTools.

func (*Manager) GetTools

func (m *Manager) GetTools() []MCPTool

GetTools returns all tools from all connected MCP servers.

func (*Manager) ListPrompts added in v0.7.0

func (m *Manager) ListPrompts(ctx context.Context, serverID string) ([]Prompt, error)

ListPrompts returns a server's prompts (prompts/list).

func (*Manager) ListResources added in v0.7.0

func (m *Manager) ListResources(ctx context.Context, serverID string) ([]Resource, error)

ListResources returns a server's resources (resources/list).

func (*Manager) ReadResource added in v0.7.0

func (m *Manager) ReadResource(ctx context.Context, serverID, uri string) ([]ResourceContent, error)

ReadResource returns the contents of one resource (resources/read).

func (*Manager) ServerInfo added in v0.11.0

func (m *Manager) ServerInfo(serverID string) (ServerInfo, bool)

ServerInfo returns what a connected server said about itself in the handshake; ok=false when it is not connected.

func (*Manager) ServerReady added in v0.7.0

func (m *Manager) ServerReady(id string) bool

ServerReady reports whether a project-scoped server finished tool discovery without error. A server with no tools at all (one that serves only resources or prompts) is ready once discovery returns empty.

func (*Manager) ServerReadyErr added in v0.4.0

func (m *Manager) ServerReadyErr(id string) error

ServerReadyErr reports why a project-scoped server discovered no tools, once discovery has finished. Returns nil while still discovering, when discovery succeeded, or when no such server is running — a caller that wants to distinguish those cases should pair it with ServerStarted.

This exists because tool discovery is async: StartServer returning nil only means the handshake worked, and a server that then fails ListTools otherwise shows up as a silent absence of tools.

func (*Manager) ServerStarted

func (m *Manager) ServerStarted(id string) bool

ServerStarted reports whether a project-scoped MCP server is already running for id (== cfg.ID). The Phase 3 decision executor uses this to spawn the write server (gmail-exec) on demand WITHOUT the idempotent-restart of StartServer tearing down a live process on every apply (which would kill in-flight IMAP/SMTP calls).

func (*Manager) ServerStderr added in v0.4.0

func (m *Manager) ServerStderr(id string) string

ServerStderr returns the tail of what a running server wrote to stderr, or "" when it has said nothing (or is not running). Useful for reporting a server that started but is misbehaving, where no error was ever returned.

func (*Manager) SetNotificationHandler added in v0.4.0

func (m *Manager) SetNotificationHandler(fn func(Notification))

SetNotificationHandler registers the sink for server→client notifications. Call before StartServer: handlers are attached at spawn, so a server already running keeps whatever sink it was spawned with.

func (*Manager) SetSecretResolver

func (m *Manager) SetSecretResolver(resolver SecretResolver)

SetSecretResolver attaches (or replaces) the resolver after construction. Used when the Manager is built before the secrets store is available.

func (*Manager) StartServer

func (m *Manager) StartServer(ctx context.Context, cfg MCPConfig) error

StartServer spawns a project-scoped MCP server process and connects to it. Idempotent: calling twice for the same cfg.ID closes the previous instance.

func (*Manager) StartThreadServer

func (m *Manager) StartThreadServer(ctx context.Context, integrationID, threadID string, cfg MCPConfig) error

StartThreadServer spawns a thread-scoped MCP server. integrationID identifies the project_integration the config came from; (integrationID, threadID) is the spawn key — a second call for the same pair restarts the server. Tool dispatch uses cfg.ID for routing, so callers should set cfg.ID = threadServerID(integID, threadID) before calling.

func (*Manager) StopServer

func (m *Manager) StopServer(id string)

StopServer disconnects and kills an MCP server.

func (*Manager) StopThreadServers

func (m *Manager) StopThreadServers(threadID string)

StopThreadServers tears down every thread-scoped MCP server bound to threadID, across all integrations. Called on thread close.

func (*Manager) ThreadServerStarted

func (m *Manager) ThreadServerStarted(integrationID, threadID string) bool

ThreadServerStarted reports whether a thread-scoped MCP server is already running for (integrationID, threadID). The scheduler's per-Turn collectTools uses this to avoid re-spawning on every tool list.

type Notification added in v0.4.0

type Notification struct {
	ServerID string
	Method   string
	Params   map[string]any
}

Notification is one server→client message, flattened to what a consumer acts on. The raw params are kept because MCP's notification methods are open-ended and this package should not become a bottleneck on which ones mean something.

func (Notification) Level added in v0.4.0

func (n Notification) Level() string

Level is the notifications/message severity, when the method carries one. Empty for methods that do not.

func (Notification) Text added in v0.4.0

func (n Notification) Text() string

Text renders the human-readable payload of a notifications/message, which is where servers put anything they want a person or a model to read. `data` is by spec any JSON value; a bare string is the common case and is returned as-is rather than re-encoded with quotes around it.

type Prompt added in v0.7.0

type Prompt struct {
	Name        string           `json:"name"`
	Title       string           `json:"title,omitempty"`
	Description string           `json:"description,omitempty"`
	Arguments   []PromptArgument `json:"arguments,omitempty"`
}

Prompt is one entry of prompts/list.

type PromptArgument added in v0.7.0

type PromptArgument struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Required    bool   `json:"required,omitempty"`
}

type PromptMessage added in v0.7.0

type PromptMessage struct {
	Role    string         `json:"role"`
	Content map[string]any `json:"content"`
}

PromptMessage is one message of a prompts/get reply. Content is one content block, kept raw for the same reason tool results are; Text renders it.

func (PromptMessage) Text added in v0.7.0

func (p PromptMessage) Text() string

type Resource added in v0.7.0

type Resource struct {
	URI         string `json:"uri"`
	Name        string `json:"name"`
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
	MimeType    string `json:"mimeType,omitempty"`
}

Resource is one entry of resources/list.

type ResourceContent added in v0.7.0

type ResourceContent struct {
	URI      string `json:"uri"`
	MimeType string `json:"mimeType,omitempty"`
	Text     string `json:"text,omitempty"`
	Blob     string `json:"blob,omitempty"`
}

ResourceContent is one entry of a resources/read reply: Text for a text resource, Blob (base64) for a binary one.

type SecretResolver

type SecretResolver interface {
	Resolve(ctx context.Context, ref string) (map[string]string, error)
}

SecretResolver resolves a secret reference (a row key in the secrets table) into a flat key/value map. The concrete implementation lives server-side (internal/server/secret_resolver.go) and wraps the at-rest *secrets.Store; mcpmgr intentionally does NOT import internal/secrets so it stays free of the crypto/db dependency edge and unit-testable with a fake resolver.

type ServerInfo added in v0.11.0

type ServerInfo struct {
	Name, Title, Version string
	ProtocolVersion      string
	Instructions         string
	Raw                  json.RawMessage
}

ServerInfo is what a server said about itself in the initialize handshake: serverInfo's name, title (newer servers) and version, the negotiated protocol version, its instructions (what it is for and how to use it, when it says), and the whole result as sent, for fields beside the spec's.

type UnauthorizedError added in v0.8.0

type UnauthorizedError struct {
	URL             string
	WWWAuthenticate string
	Body            string // the first 512 bytes of the response body
}

UnauthorizedError is a 401 that survived the refresh-and-retry. WWWAuthenticate is the challenge as the server sent it; an MCP server puts resource_metadata in it (RFC 9728), which is where OAuth discovery starts — see mcpauth.ParseWWWAuthenticate.

func (*UnauthorizedError) Error added in v0.8.0

func (e *UnauthorizedError) Error() string

func (*UnauthorizedError) Is added in v0.8.0

func (e *UnauthorizedError) Is(target error) bool

Directories

Path Synopsis
Package mcpauth has the discovery steps an MCP client needs before it can run OAuth against a protected server (MCP authorization, 2025-06-18 and later):
Package mcpauth has the discovery steps an MCP client needs before it can run OAuth against a protected server (MCP authorization, 2025-06-18 and later):

Jump to

Keyboard shortcuts

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