mcp

package
v1.2.3 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Index

Constants

View Source
const (
	MCPSessionIDHeader       = "MCP-Session-Id"
	LegacyMCPSessionIDHeader = "X-MCP-Session-ID"
)
View Source
const (
	RPCCodeSessionInvalid       = -32001
	RPCCodeServerNotInitialized = -32002
	RPCCodeOriginNotAllowed     = -32010
	RPCCodeHostNotAllowed       = -32011
	RPCCodeRateLimited          = -32012
	RPCCodeRequestTooLarge      = -32013
	RPCCodeSessionPrincipal     = -32014
	RPCCodeMethodNotAllowed     = -32015
	RPCCodeUnauthenticated      = -32020
	RPCCodeForbidden            = -32021
	RPCCodeServiceUnavailable   = -32030
)

RiskHighMask is the union of risk-class bits that gate a tool call behind a confirmation token per docs/adr/0018-risk-class-confirmation-tokens.md. The set is intentionally narrow: ordinary RiskWrite stays confirmation-free so the time-tracking surface is not slowed down by the dry-run-first flow. RiskDestructive, RiskBilling, RiskAdmin, RiskPermissionChange, and RiskExternalSideEffect each represent a "agent fires off the wrong call" failure mode that the dry-run preview is uniquely positioned to catch.

View Source
const ServerInstructions = `` /* 2376-byte string literal not displayed */

ServerInstructions is returned in the initialize response to teach MCP clients how to navigate the server. Agentic clients consume this as part of their system prompt, so it trades some verbosity for clarity.

Variables

View Source
var SupportedProtocolVersions = []string{
	"2025-11-25",
	"2025-06-18",
	"2025-03-26",
	"2024-11-05",
}

SupportedProtocolVersions lists MCP protocol versions this server can negotiate, newest first. The first entry is returned as the default when the client does not send a protocolVersion unless an operator configures a per-server default. When a client requests an unsupported version, we echo back the newest supported version — clients that cannot downgrade will treat that as an error and disconnect, which is the spec-compliant behaviour.

Functions

func DefaultProtocolVersion

func DefaultProtocolVersion(configured string) string

func IsSupportedProtocolVersion

func IsSupportedProtocolVersion(version string) bool

func RecoverDispatch

func RecoverDispatch(reqID any, site, toolHint string, sink PanicResponseSink)

RecoverDispatch is the deferred function that wraps a tool dispatch goroutine. It recovers a panic, emits a structured metric + log event, and (when a panic was caught) hands a stable JSON-RPC tool-error envelope to sink. No-op when the dispatch returns normally.

Usage at every transport's tool-dispatch site:

defer mcp.RecoverDispatch(reqID, "stdio_tool_dispatch", toolName, sink)
resp := s.handle(ctx, r)
...

Why a single helper rather than ad-hoc defer blocks at each site: stdio, streamable HTTP, and gRPC all need identical metric labelling, log shape, and response envelope. Centralising the behaviour keeps cross-transport parity tests honest and prevents regressions where one transport returns a leakier shape than another.

recover() works here because RecoverDispatch IS the deferred function — `defer fn(...)` calls fn at defer-pop time, and recover() inside fn sees the panic. Wrapping this in an IIFE at the call site (`defer func(){ RecoverDispatch(...) }()`) would break that invariant.

func ServeMetrics added in v0.6.0

func ServeMetrics(ctx context.Context, opts MetricsServerOptions) error

func ServeStreamableHTTP added in v0.6.0

func ServeStreamableHTTP(ctx context.Context, opts StreamableHTTPOptions) error

func WithNotifier

func WithNotifier(ctx context.Context, n Notifier) context.Context

WithNotifier returns a child context carrying n as the calling stream's Notifier. Transports call this once per stream after AddNotifier so the resources/subscribe handler can attribute the subscription to the right stream. Passing a nil Notifier yields the parent context unchanged.

func WithProgressToken

func WithProgressToken(ctx context.Context, token ProgressToken) context.Context

WithProgressToken attaches the client-supplied progressToken (if any) to ctx so downstream tool handlers can emit notifications/progress keyed off the same value.

Types

type Activator added in v0.6.0

type Activator interface {
	// IsGroupAllowed reports whether a Tier 2 group may be activated.
	IsGroupAllowed(group string) bool
	// OnActivate is called when tools are dynamically registered.
	OnActivate(names []string)
	// OnDeactivate is called when dynamically registered tools are removed.
	OnDeactivate(names []string)
}

Activator handles dynamic tool activation (group enable, visibility toggle). A nil Activator means activation is unrestricted.

type AuditEvent added in v0.6.0

type AuditEvent struct {
	Tool        string
	Action      string
	Outcome     string
	Phase       AuditPhase
	Reason      string
	ResourceIDs map[string]string
	Metadata    map[string]string
}

type AuditPhase added in v1.2.0

type AuditPhase = string

AuditPhase identifies which side of a non-read-only tool call an audit record was emitted on. Two-phase audit (intent → outcome) is what makes MCP_AUDIT_DURABILITY=fail_closed actually prevent mutation when audit persistence is broken: an intent record is written before the handler runs, and a fail_closed deployment short-circuits the handler when that intent persistence fails.

PhaseIntent   — pre-handler write; "we are about to call this tool"
PhaseOutcome  — post-handler write; "result was succeeded/failed"

Empty Phase ("") is preserved for backward compatibility with audit consumers that pre-date the phased model and treat every record as a single-shot outcome.

const (
	PhaseIntent  AuditPhase = "intent"
	PhaseOutcome AuditPhase = "outcome"

	// PhaseHandlerPanic identifies an outcome record written from
	// the deferred panic-recovery wrapper around the tool handler.
	// The intent record (PhaseIntent) was already written; this
	// outcome pairs with it so a crashing handler does not leave
	// an orphaned intent. Reason carries a sanitized panic value;
	// the full stack lives in the slog `panic_recovered` event.
	PhaseHandlerPanic AuditPhase = "handler_panic"
)

type Auditor added in v0.6.0

type Auditor interface {
	RecordAudit(AuditEvent) error
}

Auditor records non-read-only tool-call events for compliance and audit trail purposes. RecordAudit returns an error when persistence fails so the server can make the failure observable (log + metric) and optionally fail the call when AuditDurabilityMode is "fail_closed".

Rationale: a void return means persistence errors are silently lost. Returning an error makes audit degradation visible without mandating that every deployment fail-close on it — the server's AuditDurabilityMode field controls the actual behavior.

type Enforcement added in v0.6.0

type Enforcement interface {
	// FilterTool reports whether a tool should be listed in tools/list.
	FilterTool(name string, hints ToolHints) bool
	// BeforeCall runs before the tool handler. It may:
	//   - block the call by returning a non-nil error
	//   - short-circuit with a result (e.g., dry-run preview)
	//   - return (nil, noop, nil) to proceed normally
	// The returned release function must be called when the call completes.
	//
	// schema is the tool's advertised InputSchema (may be nil) — the
	// pipeline uses it for runtime JSON-schema validation. Pipelines that
	// receive nil skip validation and behave as before.
	BeforeCall(ctx context.Context, name string, args map[string]any, hints ToolHints, schema map[string]any, lookupHandler func(string) (ToolHandler, bool)) (result any, release func(), err error)
	// AfterCall post-processes a successful tool result (e.g., truncation).
	AfterCall(result any) (any, error)
}

Enforcement handles the tool-call enforcement pipeline. The server delegates all filtering, gating, and post-processing to this interface, keeping the protocol core free of domain logic.

A nil Enforcement means no filtering or enforcement.

type ExtraHandler added in v0.6.1

type ExtraHandler struct {
	Pattern string
	Handler http.Handler
}

ExtraHandler is a neutral pattern+handler pair that transports mount on their internal mux before ListenAndServe. The field exists so debug or observability handlers owned by cmd/ can be plugged onto the same listener as /mcp without forcing internal/mcp to import anything outside the stdlib.

Intentionally minimal: no middleware hooks, no auth toggles, no priority field. The only consumer today is the -tags=pprof build in cmd/clockify-mcp/, which mounts /debug/pprof/* against http.DefaultServeMux. Adding more knobs here means internal/mcp starts caring about debug concerns it shouldn't care about.

type HTTPAdmissionLimits added in v1.2.1

type HTTPAdmissionLimits struct {
	PerIPPerMinute        int
	PerPrincipalPerMinute int
	SSEPerSession         int
}

HTTPAdmissionLimits configures the app-layer HTTP admission guard used by legacy HTTP and streamable HTTP before JSON-RPC dispatch. These limits are intentionally process-local; hosted deployments that need cross-replica quotas must still enforce them at the gateway/load-balancer layer.

type InitializeParams

type InitializeParams struct {
	ProtocolVersion string         `json:"protocolVersion,omitempty"`
	Capabilities    map[string]any `json:"capabilities,omitempty"`
	ClientInfo      map[string]any `json:"clientInfo,omitempty"`
	Meta            *RequestMeta   `json:"_meta,omitempty"`
}

type InlineMetricsOptions added in v1.0.1

type InlineMetricsOptions struct {
	// Enabled: when false (default), /metrics is not mounted on the main
	// HTTP listener. Set MCP_HTTP_INLINE_METRICS_ENABLED=1 to opt in.
	Enabled bool
	// AuthMode controls auth for the inline /metrics endpoint.
	// "inherit_main_bearer": require the same bearer token as /mcp (default
	//   when Enabled=true and no explicit auth mode is set).
	// "static_bearer": require a separate BearerToken below.
	// "none": unauthenticated — operator must opt in explicitly; startup warns.
	AuthMode string
	// BearerToken is used when AuthMode == "static_bearer".
	BearerToken string
	// MainBearerToken is the /mcp token reused when AuthMode == "inherit_main_bearer".
	// Populated by ServeHTTP from the top-level bearerToken argument.
	MainBearerToken string
}

InlineMetricsOptions controls whether and how /metrics is exposed on the main HTTP listener when using the legacy HTTP transport (MCP_TRANSPORT=http).

The dedicated metrics listener (MCP_METRICS_BIND / ServeMetrics) is the preferred enterprise pattern because it separates the metrics scrape surface from the MCP API surface. Inline metrics on the main listener require explicit operator opt-in via MCP_HTTP_INLINE_METRICS_ENABLED=1 and an explicit auth mode — they are disabled by default.

type InvalidParamsError

type InvalidParamsError struct {
	Pointer string
	Message string
}

InvalidParamsError is returned from Enforcement.BeforeCall when the tool's input arguments fail schema validation. The tools/call dispatch translates it into a JSON-RPC -32602 (invalid params) response, with Pointer exposed under error.data.pointer so clients can locate the offending field.

Pointer is an RFC 6901 JSON Pointer (e.g. "/workspace_id"). An empty pointer means the root value itself was rejected.

func (*InvalidParamsError) Error

func (e *InvalidParamsError) Error() string

type MetricsServerOptions added in v0.6.0

type MetricsServerOptions struct {
	Bind        string
	AuthMode    string
	BearerToken string
}

type Notifier

type Notifier interface {
	Notify(method string, params any) error
}

Notifier delivers server-initiated notifications (e.g. tools/list_changed) to the connected client. Transports implement this: the stdio transport writes through the shared JSON encoder, while the legacy HTTP POST-only transport logs + counts drops until a real SSE channel is wired by the Streamable HTTP transport rewrite.

type PanicResponseSink

type PanicResponseSink func(Response)

PanicResponseSink receives the JSON-RPC response built from a recovered panic. Callers supply a sink that pushes the response onto the active transport (writeResponse for stdio, json.Encoder for streamable HTTP, gRPC SendMsg pump).

type ProgressToken

type ProgressToken = any

ProgressToken is the opaque client-supplied token echoed back on every notifications/progress. Either a string or a number per the MCP spec.

func ProgressTokenFromContext

func ProgressTokenFromContext(ctx context.Context) (ProgressToken, bool)

ProgressTokenFromContext returns the progressToken supplied in the current tools/call _meta, or (nil, false) when the client did not opt in.

type Prompt

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

Prompt is a registered prompt template with canned messages whose bodies may contain `{{name}}` placeholders substituted at prompts/get time.

type PromptArgument

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

PromptArgument describes one substitution variable a prompt accepts.

type PromptMessage

type PromptMessage struct {
	Role    string            `json:"role"`
	Content PromptMessagePart `json:"content"`
}

PromptMessage is one turn in a prompt's canned message sequence. Role is typically "user" or "assistant"; content mirrors the MCP content-part shape.

type PromptMessagePart

type PromptMessagePart struct {
	Type string `json:"type"`
	Text string `json:"text,omitempty"`
}

PromptMessagePart is a single content part inside a PromptMessage. Only text content is supported in this server; clients that want images or resource links can request a richer prompt via a follow-up tool call.

type RPCError

type RPCError struct {
	Code    int            `json:"code"`
	Message string         `json:"message"`
	Data    map[string]any `json:"data,omitempty"`
}

type Request

type Request struct {
	JSONRPC string `json:"jsonrpc"`
	ID      any    `json:"id,omitempty"`
	Method  string `json:"method"`
	Params  any    `json:"params,omitempty"`
}

type RequestMeta

type RequestMeta struct {
	ProgressToken any `json:"progressToken,omitempty"`
}

RequestMeta is the MCP _meta object that can attach side-channel hints to any request. progressToken is the only field used today — clients supply one to opt into notifications/progress from long-running tool handlers.

type Resource

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

Resource describes a concrete, static MCP resource. Dynamic (parametric) resources should be surfaced via ResourceTemplate instead.

type ResourceContents

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

ResourceContents is one chunk of resource body. Text and Blob are mutually exclusive — text/* content uses Text, binary content uses base64 in Blob.

type ResourceProvider

type ResourceProvider interface {
	ListResources(ctx context.Context) ([]Resource, error)
	ListResourceTemplates(ctx context.Context) ([]ResourceTemplate, error)
	ReadResource(ctx context.Context, uri string) ([]ResourceContents, error)
}

ResourceProvider backs the MCP resources/* method family. Implementations live outside the protocol core (tools.Service implements it for Clockify). A nil ResourceProvider on Server means the resources capability is off.

type ResourceTemplate

type ResourceTemplate struct {
	URITemplate string `json:"uriTemplate"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	MimeType    string `json:"mimeType,omitempty"`
}

ResourceTemplate describes a parametric MCP resource using RFC 6570 URI template syntax — e.g. `clockify://workspace/{workspaceId}/entry/{entryId}`.

type ResourceUpdateDelta

type ResourceUpdateDelta struct {
	// Format is one of FormatNone / FormatMerge / FormatFull /
	// FormatDeleted. Empty means do not emit a delta envelope — legacy
	// payload shape {"uri": "..."} is used.
	Format string
	// Patch is the wire-format payload for FormatMerge / FormatFull. It
	// is emitted verbatim under the "patch" key. Pre-decoded (already
	// a Go value) so marshalling the notification doesn't require a
	// re-parse step.
	Patch any
}

ResourceUpdateDelta carries the optional delta envelope the server can attach to a notifications/resources/updated payload. When Format is empty the legacy payload shape is emitted ({"uri": ...}); otherwise the envelope is merged into the notification params so MCP clients can apply a minimal JSON Merge Patch (RFC 7396) against their cached resource state instead of re-fetching the whole document.

Format values are defined in internal/jsonmergepatch (FormatNone / FormatMerge / FormatFull / FormatDeleted). The protocol core does not interpret them; it passes the envelope through to the notifier. Format validation and payload shape are the tools-layer caller's responsibility.

This extension is additive and backwards compatible: clients that only read the `uri` field keep working. No MCP protocol version bump is required. See docs/adr/0009-resource-delta-sync.md.

type Response

type Response struct {
	JSONRPC string    `json:"jsonrpc"`
	ID      any       `json:"id,omitempty"`
	Method  string    `json:"method,omitempty"`
	Result  any       `json:"result,omitempty"`
	Error   *RPCError `json:"error,omitempty"`
}

type RiskClass

type RiskClass uint32

RiskClass categorises tools beyond the three MCP boolean hints. It is a bitmask so a tool can carry multiple attributes (for example a billing action that also triggers an external side effect). Consumers — audit, policy, enforcement — pattern-match against bits to decide confirmation requirements, log fidelity, and policy gates. The taxonomy mirrors docs/policy/production-tool-scope.md.

const (
	RiskRead               RiskClass = 1 << iota // safe, idempotent reads
	RiskWrite                                    // ordinary mutating writes
	RiskBilling                                  // touches invoices / payments
	RiskAdmin                                    // workspace-admin scope (deactivate, group/user mgmt)
	RiskPermissionChange                         // role / permission changes
	RiskExternalSideEffect                       // triggers outbound delivery (email, webhook test)
	RiskDestructive                              // irreversible delete-style operations
)

func (RiskClass) Has

func (r RiskClass) Has(mask RiskClass) bool

Has reports whether the receiver carries every bit in mask.

func (RiskClass) IsHighRisk

func (r RiskClass) IsHighRisk() bool

IsHighRisk reports whether any high-risk bit is set on the receiver. Used by the confirmation-token gate (internal/enforcement) and by the schema/annotation enrichment in internal/tools/common.go.

type Server

type Server struct {
	Version     string
	Enforcement Enforcement   // nil = no filtering or enforcement
	Activator   Activator     // nil = activation unrestricted
	ToolTimeout time.Duration // per-call timeout; 0 = default 45s
	// DefaultProtocolVersion is used only when initialize omits
	// params.protocolVersion. Empty or unsupported values fall back to
	// SupportedProtocolVersions[0]; explicit supported client requests still win.
	DefaultProtocolVersion string
	ReadyChecker           func(ctx context.Context) error // optional upstream health check for /ready
	// ExposeAuthErrors controls whether HTTP transports return detailed
	// authenticator errors to unauthenticated clients. The default is false:
	// transports return a generic OAuth error_description and log details
	// server-side only.
	ExposeAuthErrors bool

	// SanitizeUpstreamErrors controls whether tool-error responses to MCP
	// clients omit upstream Clockify response bodies. The default is false
	// (verbose, useful for local development); hosted profiles
	// (shared-service, prod-postgres) set it to true so a 4xx from
	// Clockify can't leak per-tenant info across tenant boundaries.
	// The full APIError is always logged server-side regardless.
	SanitizeUpstreamErrors bool

	// ResourceProvider backs resources/* method handlers. nil disables the
	// resources capability (server omits it from initialize.result.capabilities).
	ResourceProvider ResourceProvider

	// MaxInFlightToolCalls bounds the number of concurrently-running
	// tools/call goroutines spawned by the stdio dispatch loop.
	// Acquired before the goroutine is created so bursty input cannot
	// amplify goroutine count. 0 = unlimited.
	MaxInFlightToolCalls int
	MaxMessageSize       int64

	// StrictHostCheck enables DNS rebinding protection on the HTTP
	// transport: the inbound Host header must match either a loopback
	// literal or one of the configured allowed-origin hostnames. Defaults
	// to off so that reverse-proxy deployments that rewrite Host are
	// unaffected; flip on (MCP_STRICT_HOST_CHECK=1) in localhost-bound
	// production deployments to get the strict guarantee.
	StrictHostCheck bool

	// BehindHTTPSProxy lets the baseline-header middleware emit
	// Strict-Transport-Security on plaintext responses because a
	// trusted upstream proxy is terminating TLS for us. Without TLS
	// in front of the listener and without this flag, HSTS is skipped
	// to avoid making honest http:// URLs unreachable on misconfigured
	// dev installs. Wired from MCP_BEHIND_HTTPS_PROXY=1.
	BehindHTTPSProxy bool

	// HTTPAdmissionLimits configures process-local app-layer admission
	// guards for legacy HTTP. Streamable HTTP receives the same values
	// through StreamableHTTPOptions.
	HTTPAdmissionLimits HTTPAdmissionLimits

	// ExtraHTTPHandlers carries optional handlers that the legacy HTTP
	// transport mounts on its mux before ListenAndServe. Used by the
	// -tags=pprof build in cmd/clockify-mcp/ to attach /debug/pprof/*
	// without forcing internal/mcp to depend on net/http/pprof. nil or
	// empty = no extras registered, which is the default production path.
	ExtraHTTPHandlers []ExtraHandler

	Auditor        Auditor
	AuditTenantID  string
	AuditSubject   string
	AuditSessionID string
	AuditTransport string

	// AuditDurabilityMode controls what happens when audit persistence fails
	// for a non-read-only tool call.
	//
	//   "best_effort" (default): log the error and increment the failure metric;
	//   do not fail the tool call.
	//
	//   "fail_closed": fail before mutation when the intent record cannot be
	//   persisted; post-mutation outcome failures stay log/metric-only for
	//   backwards compatibility.
	//
	//   "fail_closed_strict": same pre-mutation intent guard, plus post-mutation
	//   outcome failures are returned to the client so the missing durable
	//   outcome row is visible on the MCP wire.
	//
	//   Read-only operations are never affected regardless of this setting.
	AuditDurabilityMode string
	// contains filtered or unexported fields
}

func NewServer

func NewServer(version string, descriptors []ToolDescriptor, enforcement Enforcement, activator Activator) *Server

func (*Server) ActivateGroup added in v0.6.0

func (s *Server) ActivateGroup(groupName string, descriptors []ToolDescriptor) error

ActivateGroup registers a group of tool descriptors dynamically and sends a tools/list_changed notification to the client.

func (*Server) ActivateTier1Tool added in v0.6.0

func (s *Server) ActivateTier1Tool(name string) error

ActivateTier1Tool marks a single registered tool as visible.

func (*Server) AddNotifier

func (s *Server) AddNotifier(n Notifier) func()

AddNotifier registers a notification sink and returns a function that removes it. Multiple notifiers can coexist; Notify fans out to all of them. Transports that multiplex clients (gRPC Exchange streams) should call AddNotifier per-stream and defer the returned remove function.

func (*Server) ClientInfo

func (s *Server) ClientInfo() (name, version string)

ClientInfo returns the client name and version sent during initialize.

func (*Server) DeactivateGroup added in v1.2.1

func (s *Server) DeactivateGroup(groupName string) []string

DeactivateGroup removes descriptors that were previously registered by ActivateGroup and sends a tools/list_changed notification. The operation is idempotent: deactivating a group that is not active is a no-op.

func (*Server) DispatchMessage

func (s *Server) DispatchMessage(ctx context.Context, msg []byte) ([]byte, error)

DispatchMessage parses a single JSON-RPC message from raw bytes, invokes the central handler, and returns the serialized response. It is intended for non-stdio transports (gRPC sub-module, custom bridges) that own their own concurrency model and framing.

Parse and validation errors are converted to JSON-RPC error responses mirroring the stdio loop. A notification (no id, no result, no error) returns (nil, nil); the caller must skip sending on the wire in that case.

This method does NOT apply the stdio dispatch-layer toolCallSem. Callers that need backpressure on tools/call must implement their own bound.

func (*Server) DispatchMessageWithRecover added in v1.2.1

func (s *Server) DispatchMessageWithRecover(ctx context.Context, msg []byte, site string) ([]byte, error)

DispatchMessageWithRecover is the recovery-wrapped variant of DispatchMessage for transports whose dispatch goroutines must not let a panicking handler escape the transport boundary. gRPC's per-frame goroutine uses this so a single broken tool cannot kill an in-flight stream — every other concurrent request on the same Exchange would otherwise be aborted.

Identical to DispatchMessage on the parse / validate / marshal path; the only difference is that handle() runs through HandleWithRecover so panics are translated into the same stable JSON-RPC tool-error envelope stdio + streamable HTTP emit.

func (*Server) HandleWithRecover

func (s *Server) HandleWithRecover(ctx context.Context, req Request, site string) (resp Response)

HandleWithRecover invokes handle with structured panic recovery. Used by transports whose dispatch goroutines do not own a higher- level recovery wrapper (streamable HTTP, gRPC) — stdio's loop has its own RecoverDispatch deferred at the goroutine boundary in Run.

site is the metric/log label that lets operators distinguish where a panic originated. Returns the stable JSON-RPC tool-error response when a panic was recovered; otherwise the handler's normal Response.

Why a named-return wrapper: recover() only sees the panic when called by the deferred function directly; the sink callback then writes the panic envelope into the named return variable so the caller observes the structured response rather than the zero value. RecoverDispatch handles metric increment + slog event.

func (*Server) HasResourceSubscription

func (s *Server) HasResourceSubscription(uri string) bool

HasResourceSubscription reports whether any client is currently subscribed to uri. The tool layer calls this before re-reading a resource in emitResourceUpdate so unsubscribed mutations don't pay for a redundant ReadResource round-trip. Coarse by design — it answers "does anyone want this URI?" so the read is skipped when nobody does; the per-stream filter that prevents cross-talk happens inside NotifyResourceUpdated.

func (*Server) InFlightToolCalls

func (s *Server) InFlightToolCalls() int

InFlightToolCalls reports the current depth of the stdio dispatch semaphore. Returns 0 when the semaphore is disabled.

func (*Server) InflightCount

func (s *Server) InflightCount() int

InflightCount returns the number of tracked in-flight tools/call requests. Used by tests to verify the map is cleaned up.

func (*Server) IsReadyCached added in v0.6.0

func (s *Server) IsReadyCached() bool

IsReadyCached reports whether the last cached readiness probe resulted in success. Scrapers should prefer /ready for fresh probes; this method only reads the cached value so /metrics does not trigger upstream calls on every scrape.

func (*Server) MarkInitialized

func (s *Server) MarkInitialized(protocolVersion, clientName, clientVersion string)

MarkInitialized seeds a freshly-built Server with the negotiated state it would normally obtain from a live `initialize` exchange. Used by the streamable-HTTP cross-pod rehydration path (ADR 0017, Path A) to rebuild a session whose `initialize` ran on a different replica: the persisted controlplane.SessionRecord carries ProtocolVersion + ClientName + ClientVersion, and the rehydrated Server uses them instead of demanding the client re-initialize. Idempotent — calling it twice with the same values is a no-op.

protocolVersion may be empty (a session that was never re-persisted after sync_initialize, or a pre-2025-03-26 client); ClientInfo fields are best-effort. The initialized flag is set unconditionally because the rehydration boundary's contract is that the server is ready to dispatch tool calls; lacking a persisted protocolVersion just means validateProtocolVersion's negotiated-version check degrades to "accept any supported version" (see the comment on validateProtocolVersion in transport_streamable_http.go).

func (*Server) NegotiatedProtocolVersion added in v0.6.0

func (s *Server) NegotiatedProtocolVersion() string

NegotiatedProtocolVersion returns the MCP protocol version agreed with the client, or empty string before initialize runs.

func (*Server) Notify

func (s *Server) Notify(method string, params any) error

Notify forwards a server-initiated notification through all registered notifiers. Returns nil when no notifiers are installed.

func (*Server) NotifyResourceUpdated

func (s *Server) NotifyResourceUpdated(uri string, delta ResourceUpdateDelta)

NotifyResourceUpdated publishes notifications/resources/updated to every notifier that called resources/subscribe for uri. Transports/tool handlers call this after a mutation that invalidates a cached resource view. Safe to call before any notifier is wired — the call silently no-ops when nothing is subscribed.

Subscription scope is per-notifier (per gRPC Exchange stream / per streamable-HTTP session / the single stdio peer). A stream that did NOT subscribe to uri never receives the notification, even when another stream sharing the same *mcp.Server did. tools/list_changed continues to fan out broadcast through Server.Notify; only resources/updated takes this per-URI path.

When delta.Format is non-empty the notification params include the envelope:

{
  "uri": "clockify://workspace/ws/entry/id",
  "format": "merge",
  "patch": { "description": "new text", "billable": true }
}

Empty delta preserves the legacy payload shape {"uri": "..."} so existing clients and tests remain unchanged.

func (*Server) Run

func (s *Server) Run(ctx context.Context, r io.Reader, w io.Writer) error

Run processes JSON-RPC requests from r and writes responses to w. It respects ctx cancellation for graceful shutdown — when ctx is cancelled, the loop exits even if stdin is blocking.

func (*Server) ServeHTTP added in v0.6.0

func (s *Server) ServeHTTP(ctx context.Context, bind string, authenticator authn.Authenticator, bearerToken string, allowedOrigins []string, allowAnyOrigin bool, maxBodySize int64, inlineMetrics InlineMetricsOptions) error

ServeHTTP starts an HTTP server that wraps the MCP server's handle() method. Auth is delegated to the supplied authn.Authenticator — for legacy deployments operators pass a static-bearer authenticator built from MCP_BEARER_TOKEN; enterprise deployments can pass OIDC/forward_auth/mTLS authenticators through the same seam. bearerToken remains non-empty only because inline-metrics inheritance reuses it; nil authenticator + empty bearerToken is rejected so the handler never runs unauthenticated.

When allowAnyOrigin is false and allowedOrigins is empty, cross-origin requests are rejected (secure default).

inlineMetrics controls whether /metrics is mounted on the main listener. The default (InlineMetricsOptions{}) leaves /metrics absent — use the dedicated metrics listener (ServeMetrics) for the recommended pattern.

func (*Server) ServeHTTPListener added in v1.0.1

func (s *Server) ServeHTTPListener(ctx context.Context, ln net.Listener, authenticator authn.Authenticator, bearerToken string, allowedOrigins []string, allowAnyOrigin bool, maxBodySize int64, inlineMetrics InlineMetricsOptions) error

ServeHTTPListener is the listener-injection counterpart of ServeHTTP. It takes ownership of an already-open net.Listener (callers should use net.Listen("tcp", "127.0.0.1:0") to get an ephemeral port) and runs the same middleware stack until ctx is cancelled. Primarily for tests that need to know the bound port before the server starts accepting.

func (*Server) SetNotifier

func (s *Server) SetNotifier(n Notifier)

SetNotifier installs a notification sink, removing any previously installed via SetNotifier. Transports that own a single client (stdio, legacy HTTP) use this for backwards compatibility. Internally delegates to AddNotifier.

func (*Server) SetReadyCached added in v1.0.0

func (s *Server) SetReadyCached(ready bool)

SetReadyCached updates the cached readiness state. Transports that lack an HTTP readiness endpoint (gRPC) call this after verifying upstream connectivity so IsReadyCached reflects their state.

func (*Server) VisibleToolCount added in v1.2.1

func (s *Server) VisibleToolCount() int

VisibleToolCount reports the current tools/list count after enforcement filtering. Activators use it after a successful activation so responses can distinguish a group-local tool count from the session-visible total.

func (*Server) VisibleToolNames added in v1.2.1

func (s *Server) VisibleToolNames() map[string]bool

VisibleToolNames reports the current tools/list names after enforcement filtering. Activation responses use it to avoid promising tools that the client still cannot call because bootstrap or policy filtering hides them.

type StreamableHTTPOptions added in v0.6.0

type StreamableHTTPOptions struct {
	Version string
	Bind    string
	// Listener, if non-nil, is used in place of net.Listen("tcp", Bind).
	// Primarily for tests that need to know the bound port before the
	// server starts accepting. Bind is still consulted for the logged
	// startup address when Listener is nil.
	Listener        net.Listener
	MaxBodySize     int64
	AllowedOrigins  []string
	AllowAnyOrigin  bool
	StrictHostCheck bool
	// ExposeAuthErrors controls whether unauthenticated clients receive
	// detailed authenticator failure reasons. Default false returns a
	// generic OAuth error_description and logs details server-side only.
	ExposeAuthErrors bool
	// SanitizeUpstreamErrors controls whether tool errors returned to
	// MCP clients omit upstream Clockify response bodies. Default false
	// (verbose, useful for local development); hosted profiles flip it
	// on so upstream payloads cannot cross tenant boundaries.
	SanitizeUpstreamErrors bool
	SessionTTL             time.Duration
	ReadyChecker           func(context.Context) error
	Authenticator          authn.Authenticator
	ControlPlane           controlplane.Store
	Factory                StreamableSessionFactory
	// ProtectedResource is the unauthenticated handler for the
	// /.well-known/oauth-protected-resource metadata document. When
	// non-nil it is mounted at the canonical RFC 9728 path. nil =
	// endpoint omitted (e.g. server does not advertise OAuth 2.1
	// resource discovery).
	ProtectedResource http.Handler
	// ExtraHandlers mounts optional handlers on the streamable HTTP
	// mux before ListenAndServe — counterpart to Server.ExtraHTTPHandlers
	// for the streamable transport. Used by -tags=pprof to attach
	// /debug/pprof/* alongside /mcp. nil = no extras, default path.
	ExtraHandlers []ExtraHandler
	// POSTWriteTimeout bounds writes for request/response POST /mcp calls.
	// The http.Server keeps WriteTimeout=0 so long-lived SSE streams are not
	// severed; this per-request deadline keeps stalled POST clients from
	// pinning a handler indefinitely. Zero uses
	// defaultStreamablePOSTWriteTimeout.
	POSTWriteTimeout time.Duration
	// IdleGraceAfterDisconnect is the maximum time a session with zero
	// active SSE subscribers may sit before the reaper evicts it early.
	// Guards against orphaned-subscriber leaks where a client drops TCP
	// mid-stream without DELETEing the session: SessionTTL alone would
	// hold the entry for up to 30 minutes. Zero uses the 5 minute default.
	IdleGraceAfterDisconnect time.Duration
	// TLSConfig, when non-nil, wraps the bound listener with tls.NewListener
	// so the streamable HTTP transport terminates TLS in-process. Required
	// when the operator selects MCP_AUTH_MODE=mtls on streamable_http —
	// without TLS, the mTLS authenticator has no VerifiedChains to read
	// from r.TLS and every request fails with "verified mTLS client
	// certificate required". When nil, the listener serves plain HTTP
	// (the long-standing default for this transport).
	TLSConfig *tls.Config
	// BehindHTTPSProxy, when true, lets the baseline-header middleware
	// emit Strict-Transport-Security on plaintext responses because a
	// trusted upstream proxy is terminating TLS for us. Without TLS in
	// front of the listener and without this flag, HSTS is skipped to
	// avoid making honest http:// URLs unreachable on misconfigured
	// dev installs. Wired from MCP_BEHIND_HTTPS_PROXY=1.
	BehindHTTPSProxy bool
	// AdmissionLimits configures process-local HTTP admission guards for
	// POST /mcp and GET /mcp. These limits protect the application before
	// JSON-RPC dispatch; multi-replica hosted deployments still need
	// cross-replica gateway limits.
	AdmissionLimits HTTPAdmissionLimits
	// MaxSessionsPerReplica caps the number of active streamable_http
	// sessions on this replica. initialize requests beyond the cap return
	// HTTP 503 with Retry-After. 0 = unlimited (matches the legacy
	// behaviour before this knob existed). Pair with MaxSessionsPerPrincipal
	// to prevent a single tenant from starving the pool.
	MaxSessionsPerReplica int
	// MaxSessionsPerPrincipal caps active sessions per authenticated
	// (tenant_id, subject) on this replica. initialize requests beyond
	// the cap return HTTP 429 with Retry-After so a tenant-internal
	// retry loop can back off without affecting other tenants. 0 =
	// unlimited.
	MaxSessionsPerPrincipal int
	// RequireProtocolVersionHeader, when true, rejects post-initialize
	// streamable HTTP requests that omit Mcp-Protocol-Version. Default false
	// preserves compatibility with older clients while still counting missing
	// headers for operator migration visibility.
	RequireProtocolVersionHeader bool
	// DefaultProtocolVersion is the fallback initialize protocolVersion when a
	// client omits params.protocolVersion. Empty means newest supported.
	DefaultProtocolVersion string
}

type StreamableSessionFactory added in v0.6.0

type StreamableSessionFactory func(context.Context, authn.Principal, string) (*StreamableSessionRuntime, error)

type StreamableSessionRuntime added in v0.6.0

type StreamableSessionRuntime struct {
	Server          *Server
	Close           func()
	TenantID        string
	WorkspaceID     string
	ClockifyBaseURL string
}

type Tool

type Tool struct {
	Name         string         `json:"name"`
	Description  string         `json:"description"`
	InputSchema  map[string]any `json:"inputSchema,omitempty"`
	OutputSchema map[string]any `json:"outputSchema,omitempty"`
	Annotations  map[string]any `json:"annotations,omitempty"`
}

type ToolCallParams

type ToolCallParams struct {
	Name      string         `json:"name"`
	Arguments map[string]any `json:"arguments,omitempty"`
	Meta      *RequestMeta   `json:"_meta,omitempty"`
}

type ToolDescriptor

type ToolDescriptor struct {
	Tool            Tool
	Handler         ToolHandler
	ReadOnlyHint    bool
	DestructiveHint bool
	IdempotentHint  bool
	// RiskClass is the structured risk taxonomy; defaults are derived from
	// the boolean hints in normalizeDescriptors when this field is zero.
	// Tier-2 tools that need finer granularity than read/write/destructive
	// (billing, admin, permission_change, external_side_effect) override it
	// in internal/tools/risk_overrides.go.
	RiskClass RiskClass
	// AuditKeys lists argument keys (beyond the implicit *_id suffix scan)
	// whose values must be captured in audit events. Used for action-defining
	// fields like role, status, quantity, unit_price.
	AuditKeys []string
}

type ToolHandler

type ToolHandler func(context.Context, map[string]any) (any, error)

type ToolHints added in v0.6.0

type ToolHints struct {
	ReadOnly    bool
	Destructive bool
	Idempotent  bool
	// AuditKeys is forwarded from ToolDescriptor.AuditKeys so the audit
	// recorder can capture action-defining argument values (role, status,
	// quantity, unit_price, …) beyond the implicit *_id suffix scan in
	// resourceIDs. Empty for tools whose argument shape is fully
	// described by *_id keys.
	AuditKeys []string
	// RiskClass is the bitmask risk taxonomy populated on every
	// ToolDescriptor by internal/tools/applyRiskMetadata. It is forwarded
	// here so Enforcement.FilterTool / BeforeCall can make policy
	// decisions on bits (RiskBilling, RiskAdmin, RiskPermissionChange,
	// RiskExternalSideEffect, RiskDestructive) rather than re-deriving
	// the class from the tool name string. This is the Q3 prerequisite
	// from docs/adr/0018-risk-class-confirmation-tokens.md — it adds
	// no behaviour today, just the surface that future
	// confirmation-token or risk-bit-aware gates will consume. Zero
	// means "no risk classification available" (legacy callers that
	// build the literal without a descriptor).
	RiskClass RiskClass
}

ToolHints carries semantic hints about a tool's behavior.

type UnknownToolError

type UnknownToolError struct {
	Name string
}

func (*UnknownToolError) Error

func (e *UnknownToolError) Error() string

Jump to

Keyboard shortcuts

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