mcp

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Dec 3, 2025 License: MIT Imports: 20 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EnableWithMCP

func EnableWithMCP(config *Config) (*devtools.DevTools, error)

EnableWithMCP creates and enables the dev tools singleton with MCP server integration.

This function combines devtools.Enable() with MCP server initialization, providing a convenient way to start dev tools with AI-assisted debugging capabilities enabled.

The MCP server exposes devtools data via the Model Context Protocol, allowing AI agents to inspect components, state, events, and performance metrics in real-time.

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

// Enable with stdio transport (default)
cfg := mcp.DefaultMCPConfig()
dt, err := mcp.EnableWithMCP(cfg)
if err != nil {
    log.Fatal(err)
}

// Enable with HTTP transport
cfg := &mcp.Config{
    Transport:  mcp.MCPTransportHTTP,
    HTTPPort:   8765,
    EnableAuth: true,
    AuthToken:  "secret-token",
}
dt, err := mcp.EnableWithMCP(cfg)
if err != nil {
    log.Fatal(err)
}

Parameters:

  • config: MCP server configuration

Returns:

  • *devtools.DevTools: The singleton dev tools instance with MCP enabled
  • error: Configuration validation error, or MCP server creation error

func SanitizeInput

func SanitizeInput(input string) string

SanitizeInput removes dangerous characters from user input.

This function removes or replaces:

  • SQL injection characters: ; ' "
  • Command injection: ` $ ( ) | & < >
  • Path traversal: ../
  • Null bytes: \x00
  • Control characters (except space and tab)

Note: This is defense-in-depth. Primary defense is proper parameterization and validation. Use this for logging and display purposes.

Example:

input := "'; DROP TABLE users; --"
safe := SanitizeInput(input)  // "' DROP TABLE users --"

Parameters:

  • input: The string to sanitize

Returns:

  • string: Sanitized string with dangerous characters removed

func ValidateResourceURI

func ValidateResourceURI(uri string) error

func ValidateToolParams

func ValidateToolParams(toolName string, params map[string]interface{}) error

ValidateToolParams validates parameters for MCP tool calls.

This function performs tool-specific validation to prevent injection attacks:

  • export_session: format, destination path, include sections
  • search_components: query, fields, max_results
  • filter_events: event_names, source_ids, limit
  • set_ref_value: ref_id, new_value
  • clear_state_history: no params
  • clear_event_log: no params

Security Features:

  • SQL injection prevention
  • Command injection prevention
  • Path traversal prevention
  • Parameter type validation
  • Range validation

Example:

params := map[string]interface{}{
    "format": "json",
    "destination": "/tmp/export.json",
}
err := ValidateToolParams("export_session", params)

Parameters:

  • toolName: The name of the tool being called
  • params: The parameters passed to the tool

Returns:

  • error: nil if valid, descriptive error otherwise

Types

type AuthHandler

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

AuthHandler provides bearer token authentication for HTTP transport.

The handler implements HTTP middleware that validates bearer tokens in the Authorization header. It uses constant-time comparison to prevent timing attacks and never logs or exposes tokens in error messages.

Security Features:

  • Constant-time token comparison (timing attack resistant)
  • Token sanitization in error messages
  • Configurable enable/disable
  • Thread-safe operation

Example:

auth, err := NewAuthHandler("secret-token-123", true)
if err != nil {
    log.Fatal(err)
}

mux := http.NewServeMux()
mux.Handle("/api", auth.Middleware(apiHandler))

http.ListenAndServe(":8080", mux)

func NewAuthHandler

func NewAuthHandler(token string, enabled bool) (*AuthHandler, error)

NewAuthHandler creates a new authentication handler.

The handler validates bearer tokens in the Authorization header using constant-time comparison to prevent timing attacks. When enabled is false, all requests are allowed through without authentication.

Parameters:

  • token: The bearer token to validate against (required if enabled is true)
  • enabled: Whether authentication is enabled

Returns:

  • *AuthHandler: The configured authentication handler
  • error: Validation error if token is empty when enabled is true

Example:

// Enable authentication
auth, err := NewAuthHandler("my-secret-token", true)
if err != nil {
    log.Fatal(err)
}

// Disable authentication (for development)
auth, err := NewAuthHandler("", false)

func (*AuthHandler) Middleware

func (a *AuthHandler) Middleware(next http.Handler) http.Handler

Middleware wraps an HTTP handler with bearer token authentication.

The middleware validates the Authorization header and only calls the next handler if authentication succeeds. When authentication is disabled, all requests pass through without validation.

Authentication Flow:

  1. If auth is disabled, call next handler immediately
  2. Extract Authorization header
  3. Validate "Bearer <token>" format
  4. Compare token using constant-time comparison
  5. Return 401 Unauthorized on failure
  6. Call next handler on success

Security:

  • Uses subtle.ConstantTimeCompare to prevent timing attacks
  • Never includes tokens in error messages
  • Returns generic error messages to prevent information leakage

Parameters:

  • next: The HTTP handler to call if authentication succeeds

Returns:

  • http.Handler: The wrapped handler with authentication

Example:

auth, _ := NewAuthHandler("secret", true)
protectedHandler := auth.Middleware(myHandler)
http.Handle("/api", protectedHandler)

type ClearEventLogParams

type ClearEventLogParams struct {
	// Confirm is required to be true to prevent accidental deletion
	Confirm bool `json:"confirm"`
}

ClearEventLogParams defines the parameters for the clear_event_log tool.

This structure is used by AI agents to specify options when clearing the event log.

Example:

{
  "confirm": true
}

type ClearResult

type ClearResult struct {
	// Cleared is the number of items that were cleared
	Cleared int `json:"cleared"`

	// Timestamp is when the clear operation completed
	Timestamp time.Time `json:"timestamp"`
}

ClearResult contains the result of a clear operation.

This structure is returned to AI agents after a successful clear operation.

Example:

{
  "cleared": 150,
  "timestamp": "2025-01-13T14:30:22Z"
}

type ClearStateHistoryParams

type ClearStateHistoryParams struct {
	// Confirm is required to be true to prevent accidental deletion
	Confirm bool `json:"confirm"`
}

ClearStateHistoryParams defines the parameters for the clear_state_history tool.

This structure is used by AI agents to specify options when clearing state history.

Example:

{
  "confirm": true
}

type ComponentMatch

type ComponentMatch struct {
	// ID is the component's unique identifier
	ID string `json:"id"`

	// Name is the component's name
	Name string `json:"name"`

	// Type is the component's type
	Type string `json:"type"`

	// Status is the component's lifecycle status
	Status string `json:"status"`

	// MatchScore is a relevance score (0.0 to 1.0)
	MatchScore float64 `json:"match_score"`

	// MatchedField is which field matched the query
	MatchedField string `json:"matched_field"`
}

ComponentMatch represents a single component that matched the search.

type ComponentsResource

type ComponentsResource struct {
	// Roots contains all root-level components (components without parents)
	Roots []*devtools.ComponentSnapshot `json:"roots"`

	// TotalCount is the total number of components in the tree
	TotalCount int `json:"total_count"`

	// Timestamp indicates when this snapshot was captured
	Timestamp time.Time `json:"timestamp"`
}

ComponentsResource represents the full component tree resource.

This structure is returned by the bubblyui://components resource and provides a snapshot of all root components in the application.

JSON Schema:

{
  "roots": [ComponentSnapshot],
  "total_count": int,
  "timestamp": string (ISO 8601)
}

type ComputedInfo

type ComputedInfo struct {
	// ID is the unique identifier of the computed value
	ID string `json:"id"`

	// Name is the variable name
	Name string `json:"name"`

	// Type is the Go type
	Type string `json:"type"`

	// Value is the current computed value
	Value interface{} `json:"value"`

	// Dependencies lists the ref IDs this computed value depends on
	Dependencies []string `json:"dependencies,omitempty"`
}

ComputedInfo provides information about computed values (future enhancement).

This structure will be populated in future versions when computed value tracking is fully implemented in the devtools system.

type Config

type Config struct {
	// Transport specifies which transport(s) to enable
	// Can be MCPTransportStdio, MCPTransportHTTP, or both (bitwise OR)
	Transport TransportType

	// HTTPPort is the port for HTTP transport (1-65535)
	// Only used when MCPTransportHTTP is enabled
	HTTPPort int

	// HTTPHost is the host to bind HTTP server to
	// Use "localhost" for local-only, "0.0.0.0" for all interfaces
	// Only used when MCPTransportHTTP is enabled
	HTTPHost string

	// WriteEnabled allows state modification tools (set_ref_value, etc.)
	// DANGER: Enable only for testing, not production debugging
	// Default: false (read-only access)
	WriteEnabled bool

	// MaxClients limits concurrent client connections
	// Prevents resource exhaustion from too many clients
	// Default: 5
	MaxClients int

	// SubscriptionThrottle is minimum time between subscription updates
	// Prevents overwhelming clients with high-frequency changes
	// Default: 100ms
	SubscriptionThrottle time.Duration

	// RateLimit is maximum requests per second per client
	// Prevents DoS attacks and resource exhaustion
	// Default: 60 req/sec
	RateLimit int

	// EnableAuth requires bearer token authentication for HTTP transport
	// Recommended for any non-localhost HTTP access
	// Default: false
	EnableAuth bool

	// AuthToken is the bearer token for HTTP authentication
	// Required when EnableAuth is true
	// Should be a strong random string (e.g., UUID)
	AuthToken string

	// SanitizeExports automatically removes PII from exported data
	// Recommended to keep enabled for security
	// Default: true
	SanitizeExports bool
}

Config holds configuration options for the MCP server.

Configuration controls transport selection, security settings, performance tuning, and write operation permissions. All fields have sensible defaults via DefaultMCPConfig().

Thread Safety:

Config instances are not thread-safe. Create separate instances for
concurrent use or protect access with a mutex.

Example:

// Use defaults (stdio transport, read-only)
cfg := mcp.DefaultMCPConfig()

// Enable HTTP transport with auth
cfg := &mcp.Config{
    Transport:  mcp.MCPTransportHTTP,
    HTTPPort:   8765,
    HTTPHost:   "localhost",
    EnableAuth: true,
    AuthToken:  "secret-token",
}

// Validate before use
if err := cfg.Validate(); err != nil {
    log.Fatal(err)
}

func DefaultMCPConfig

func DefaultMCPConfig() *Config

DefaultMCPConfig returns an Config with sensible default values.

The defaults are optimized for local development with stdio transport:

  • Stdio transport only (no network exposure)
  • HTTP port 8765 (if HTTP enabled later)
  • Localhost binding (no remote access)
  • Read-only access (no state modification)
  • 5 max concurrent clients
  • 100ms subscription throttle
  • 60 requests/second rate limit
  • No authentication (stdio is local-only)
  • Sanitization enabled

These defaults can be modified as needed for specific use cases.

Example:

cfg := mcp.DefaultMCPConfig()
cfg.Transport = mcp.MCPTransportHTTP  // Enable HTTP
cfg.EnableAuth = true                 // Require auth
cfg.AuthToken = "secret-token"        // Set token
if err := cfg.Validate(); err != nil {
    log.Fatal(err)
}

Returns:

  • *Config: A new config with default values

func (*Config) Validate

func (c *Config) Validate() error

Validate checks that the configuration values are valid.

This method verifies that:

  • HTTP port is in valid range (1-65535) when HTTP transport enabled
  • HTTP host is not empty when HTTP transport enabled
  • Max clients is positive
  • Rate limit is positive
  • Subscription throttle is non-negative
  • Auth token is provided when auth is enabled

Call this after creating or modifying config to ensure validity.

Example:

cfg := mcp.DefaultMCPConfig()
cfg.HTTPPort = 0  // Invalid
if err := cfg.Validate(); err != nil {
    log.Printf("Invalid config: %v", err)
}

Returns:

  • error: Validation error, or nil if config is valid

type EventsResource

type EventsResource struct {
	// Events contains all event records
	Events []devtools.EventRecord `json:"events"`

	// TotalCount is the total number of events
	TotalCount int `json:"total_count"`

	// Timestamp indicates when this snapshot was captured
	Timestamp time.Time `json:"timestamp"`
}

EventsResource represents the events log resource.

This structure is returned by the bubblyui://events/log resource and provides a snapshot of all events that occurred in the application.

JSON Schema:

{
  "events": [EventRecord],
  "total_count": int,
  "timestamp": string (ISO 8601)
}

type ExportParams

type ExportParams struct {
	// Format specifies the export format: "json", "yaml", or "msgpack"
	Format string `json:"format"`

	// Compress enables gzip compression of the export file
	Compress bool `json:"compress"`

	// Sanitize enables redaction of sensitive data
	Sanitize bool `json:"sanitize"`

	// Include specifies which data sections to include
	// Valid values: "components", "state", "events", "performance"
	Include []string `json:"include"`

	// Destination is the file path where the export will be saved
	// Use "stdout" to write to standard output (not recommended for large exports)
	Destination string `json:"destination"`
}

ExportParams defines the parameters for the export_session tool.

This structure is used by AI agents to specify export options when calling the export_session tool via MCP.

Example:

{
  "format": "json",
  "compress": true,
  "sanitize": true,
  "include": ["components", "state", "events"],
  "destination": "/tmp/debug-session.json.gz"
}

type ExportResult

type ExportResult struct {
	// Path is the absolute path to the exported file
	Path string `json:"path"`

	// Size is the file size in bytes
	Size int64 `json:"size"`

	// Format is the export format used
	Format string `json:"format"`

	// Compressed indicates if the file is gzip compressed
	Compressed bool `json:"compressed"`

	// Timestamp is when the export was created
	Timestamp time.Time `json:"timestamp"`
}

ExportResult contains the result of an export operation.

This structure is returned to AI agents after a successful export.

Example:

{
  "path": "/tmp/debug-session.json.gz",
  "size": 245678,
  "format": "json",
  "compressed": true,
  "timestamp": "2025-01-13T14:30:22Z"
}

type FilterEventsParams

type FilterEventsParams struct {
	// EventNames filters by event name (empty = all events)
	EventNames []string `json:"event_names"`

	// SourceIDs filters by source component ID (empty = all sources)
	SourceIDs []string `json:"source_ids"`

	// StartTime filters events after this time (optional)
	StartTime *time.Time `json:"start_time"`

	// EndTime filters events before this time (optional)
	EndTime *time.Time `json:"end_time"`

	// Limit limits the number of results returned (default: 100)
	Limit int `json:"limit"`
}

FilterEventsParams defines the parameters for the filter_events tool.

This structure is used by AI agents to specify filtering criteria when querying the event log.

Example:

{
  "event_names": ["click", "submit"],
  "source_ids": ["comp-1"],
  "start_time": "2025-01-13T14:00:00Z",
  "end_time": "2025-01-13T15:00:00Z",
  "limit": 100
}

type FilterEventsResult

type FilterEventsResult struct {
	// Events contains the filtered events
	Events []devtools.EventRecord `json:"events"`

	// TotalMatches is the number of events that matched the filter
	TotalMatches int `json:"total_matches"`

	// FilteredFrom is the total number of events before filtering
	FilteredFrom int `json:"filtered_from"`

	// Timestamp is when the filter was performed
	Timestamp time.Time `json:"timestamp"`
}

FilterEventsResult contains the result of an event filtering operation.

This structure is returned to AI agents after a successful filter.

Example:

{
  "events": [
    {
      "id": "event-1",
      "name": "click",
      "source_id": "comp-1",
      "timestamp": "2025-01-13T14:30:22Z"
    }
  ],
  "total_matches": 1,
  "filtered_from": 150,
  "timestamp": "2025-01-13T14:30:22Z"
}

type FlushHandler

type FlushHandler func(clientID string, updates []UpdateNotification)

FlushHandler is called when a batch of updates is ready to be sent.

The handler receives the client ID and a slice of updates to send. It should send the updates to the client via the MCP protocol.

Thread Safety:

Handlers may be called concurrently for different clients.

Example:

handler := func(clientID string, updates []UpdateNotification) {
    for _, update := range updates {
        // Send update to client via MCP
        server.SendNotification(clientID, update)
    }
}

type NotificationSender

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

NotificationSender sends resource update notifications to MCP clients.

It integrates with the UpdateBatcher to queue notifications for batching and throttling, preventing client overload from high-frequency updates.

Thread Safety:

All methods are thread-safe and can be called concurrently.

Lifecycle:

  1. NewNotificationSender() - Creates the sender with a batcher
  2. QueueNotification() - Queue notifications for batching
  3. ... Batcher handles flushing and sending ...

Example:

batcher, err := NewUpdateBatcher(100*time.Millisecond, 10)
if err != nil {
    log.Fatal(err)
}

notifier, err := NewNotificationSender(batcher)
if err != nil {
    log.Fatal(err)
}

// Queue a notification
notifier.QueueNotification("client-1", "bubblyui://state/refs", map[string]interface{}{
    "ref_id": "count-ref",
    "value":  42,
})

func NewNotificationSender

func NewNotificationSender(batcher *UpdateBatcher) (*NotificationSender, error)

NewNotificationSender creates a new notification sender.

The sender uses the provided batcher to queue notifications for batching and throttling. The batcher must have a flush handler configured to actually send the notifications.

Thread Safety:

Safe to call concurrently (creates new instance each time).

Example:

batcher, err := NewUpdateBatcher(100*time.Millisecond, 10)
if err != nil {
    log.Fatal(err)
}

notifier, err := NewNotificationSender(batcher)
if err != nil {
    log.Fatal(err)
}

Parameters:

  • batcher: The update batcher to use for queuing notifications

Returns:

  • *NotificationSender: A new notification sender instance
  • error: Validation error, or nil on success

func (*NotificationSender) QueueNotification

func (n *NotificationSender) QueueNotification(clientID, uri string, data map[string]interface{})

QueueNotification queues a notification for batching and sending.

The notification will be added to the batcher, which will flush it either after the flush interval or when the batch size is reached.

This method does not block - it queues the notification and returns immediately. The actual sending is handled asynchronously by the batcher.

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

notifier.QueueNotification("client-1", "bubblyui://state/refs", map[string]interface{}{
    "ref_id": "count-ref",
    "value":  42,
})

Parameters:

  • clientID: The client to send the notification to
  • uri: The resource URI that changed
  • data: The notification payload (can be nil)

type PerformanceResource

type PerformanceResource struct {
	// Components maps component ID to performance metrics
	Components map[string]*devtools.ComponentPerformance `json:"components"`

	// Summary provides aggregated performance statistics
	Summary *PerformanceSummary `json:"summary"`

	// Timestamp indicates when this snapshot was captured
	Timestamp time.Time `json:"timestamp"`
}

PerformanceResource represents the performance metrics resource.

This structure is returned by the bubblyui://performance/metrics resource and provides a comprehensive view of component performance metrics.

JSON Schema:

{
  "components": {
    "component-id": ComponentPerformance
  },
  "summary": PerformanceSummary,
  "timestamp": string (ISO 8601)
}

type PerformanceSummary

type PerformanceSummary struct {
	// TotalComponents is the total number of components tracked
	TotalComponents int `json:"total_components"`

	// TotalRenders is the total number of renders across all components
	TotalRenders int64 `json:"total_renders"`

	// SlowestComponent is the ID of the component with the slowest max render time
	SlowestComponent string `json:"slowest_component,omitempty"`

	// SlowestRenderTime is the slowest max render time
	SlowestRenderTime time.Duration `json:"slowest_render_time,omitempty"`

	// FastestComponent is the ID of the component with the fastest min render time
	FastestComponent string `json:"fastest_component,omitempty"`

	// FastestRenderTime is the fastest min render time
	FastestRenderTime time.Duration `json:"fastest_render_time,omitempty"`

	// MostRenderedComponent is the ID of the component with the most renders
	MostRenderedComponent string `json:"most_rendered_component,omitempty"`

	// MostRenderedCount is the number of renders for the most rendered component
	MostRenderedCount int64 `json:"most_rendered_count,omitempty"`
}

PerformanceSummary provides aggregated performance statistics.

This structure summarizes performance metrics across all components, identifying the slowest, fastest, and most rendered components.

type RateLimiter

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

RateLimiter implements per-client rate limiting for HTTP requests.

It uses the token bucket algorithm via golang.org/x/time/rate to enforce request rate limits on a per-client basis. Each client (identified by IP) gets their own rate limiter instance, ensuring fair resource allocation and preventing DoS attacks.

Thread Safety:

All methods are thread-safe and can be called concurrently.

Example:

rl, err := NewRateLimiter(10) // 10 requests per second per client
if err != nil {
    log.Fatal(err)
}

mux := http.NewServeMux()
mux.Handle("/api", rl.Middleware(apiHandler))

func NewRateLimiter

func NewRateLimiter(requestsPerSecond int) (*RateLimiter, error)

NewRateLimiter creates a new rate limiter with the specified requests per second.

The rate limiter uses a token bucket algorithm where each client can make up to `requestsPerSecond` requests per second, with a burst capacity of 2x the rate limit to allow for bursty traffic patterns.

Parameters:

  • requestsPerSecond: Maximum requests per second per client (must be > 0)

Returns:

  • *RateLimiter: Configured rate limiter instance
  • error: Validation error if requestsPerSecond is invalid

Example:

rl, err := NewRateLimiter(100) // 100 req/s per client
if err != nil {
    log.Fatal(err)
}

func (*RateLimiter) Middleware

func (rl *RateLimiter) Middleware(next http.Handler) http.Handler

Middleware wraps an HTTP handler with rate limiting.

Requests exceeding the rate limit receive a 429 Too Many Requests response. The client IP is extracted from the request (supporting X-Forwarded-For and X-Real-IP headers for proxy scenarios).

Parameters:

  • next: The HTTP handler to wrap

Returns:

  • http.Handler: Wrapped handler with rate limiting

Example:

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
})
limited := rl.Middleware(handler)
http.ListenAndServe(":8080", limited)

type RefInfo

type RefInfo struct {
	// ID is the unique identifier of the ref
	ID string `json:"id"`

	// Name is the variable name of the ref
	Name string `json:"name"`

	// Type is the Go type of the ref's value
	Type string `json:"type"`

	// Value is the current value of the ref
	Value interface{} `json:"value"`

	// OwnerID is the ID of the component that owns this ref
	OwnerID string `json:"owner_id"`

	// OwnerName is the name of the component that owns this ref
	OwnerName string `json:"owner_name,omitempty"`

	// Watchers is the number of active watchers on this ref
	Watchers int `json:"watchers"`
}

RefInfo provides detailed information about a reactive reference.

This structure includes the ref's identity, value, type, ownership, and watcher information for debugging and analysis.

type SearchComponentsParams

type SearchComponentsParams struct {
	// Query is the search term to match against component fields
	Query string `json:"query"`

	// Fields specifies which fields to search in: "name", "type", "id"
	// If empty, searches all fields
	Fields []string `json:"fields"`

	// MaxResults limits the number of results returned (default: 50)
	MaxResults int `json:"max_results"`
}

SearchComponentsParams defines the parameters for the search_components tool.

This structure is used by AI agents to specify search criteria when searching for components in the component tree.

Example:

{
  "query": "counter",
  "fields": ["name", "type"],
  "max_results": 10
}

type SearchComponentsResult

type SearchComponentsResult struct {
	// Matches contains the components that matched the search
	Matches []ComponentMatch `json:"matches"`

	// TotalMatches is the total number of matches found
	TotalMatches int `json:"total_matches"`

	// Query is the search query that was used
	Query string `json:"query"`

	// Timestamp is when the search was performed
	Timestamp time.Time `json:"timestamp"`
}

SearchComponentsResult contains the result of a component search operation.

This structure is returned to AI agents after a successful search.

Example:

{
  "matches": [
    {
      "id": "comp-1",
      "name": "Counter",
      "type": "Counter",
      "match_score": 1.0,
      "matched_field": "name"
    }
  ],
  "total_matches": 1,
  "query": "counter",
  "timestamp": "2025-01-13T14:30:22Z"
}

type Server

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

Server is the main MCP server instance that exposes BubblyUI devtools data and capabilities to AI agents via the Model Context Protocol.

The server provides:

  • Resources: Read-only access to component tree, state, events, performance
  • Tools: Actions like export, search, clear history, state modification
  • Subscriptions: Real-time updates on component/state/event changes

Thread Safety:

All methods are thread-safe and can be called concurrently.

Lifecycle:

  1. NewMCPServer() - Creates and initializes the server
  2. StartStdioServer() or StartHTTPServer() - Starts transport (Task 1.2/1.3)
  3. ... AI agents connect and interact ...
  4. Shutdown() - Graceful cleanup (Task 7.1)

Example:

dt := devtools.Enable()
cfg := mcp.DefaultMCPConfig()
server, err := mcp.NewMCPServer(cfg, dt)
if err != nil {
    log.Fatal(err)
}

// Start transport (Task 1.2)
if err := server.StartStdioServer(ctx); err != nil {
    log.Fatal(err)
}

func NewMCPServer

func NewMCPServer(config *Config, dt *devtools.DevTools) (*Server, error)

NewMCPServer creates and initializes a new MCP server.

This function:

  • Validates the configuration
  • Validates the devtools instance
  • Creates the MCP SDK server with proper implementation details
  • Stores references to devtools and store for resource handlers

The server is created but not started. Call StartStdioServer() or StartHTTPServer() to begin accepting connections (Task 1.2/1.3).

Thread Safety:

Safe to call concurrently (creates new instance each time).

Example:

dt := devtools.Enable()
cfg := mcp.DefaultMCPConfig()

server, err := mcp.NewMCPServer(cfg, dt)
if err != nil {
    log.Fatalf("Failed to create MCP server: %v", err)
}

Parameters:

  • config: MCP server configuration (transport, security, performance)
  • dt: DevTools instance to expose via MCP

Returns:

  • *Server: Initialized server ready to start transport
  • error: Validation error, or nil on success

func (*Server) GetConfig

func (s *Server) GetConfig() *Config

GetConfig returns the server's configuration.

The returned config is the same instance passed to NewMCPServer(). Do not modify the returned config - it's shared.

Thread Safety:

Safe to call concurrently.

Example:

cfg := server.GetConfig()
fmt.Printf("Transport: %s\n", cfg.Transport)

Returns:

  • *Config: The server's configuration

func (*Server) GetDevTools

func (s *Server) GetDevTools() *devtools.DevTools

GetDevTools returns the DevTools instance.

This provides access to the full DevTools API for advanced use cases.

Thread Safety:

Safe to call concurrently.

Example:

dt := server.GetDevTools()
if dt.IsEnabled() {
    fmt.Println("DevTools active")
}

Returns:

  • *devtools.DevTools: The DevTools instance

func (*Server) GetHTTPAddr

func (s *Server) GetHTTPAddr() string

GetHTTPAddr returns the actual address the HTTP server is listening on. This is useful when using port 0 (random port assignment).

Returns empty string if HTTP server is not running.

Note: This is a helper method for testing. In production, the port should be configured explicitly.

func (*Server) GetHTTPPort

func (s *Server) GetHTTPPort() int

GetHTTPPort returns the actual port the HTTP server is listening on. Returns 0 if HTTP server is not running or port is not yet assigned.

This is useful for testing with random port assignment (port 0).

func (*Server) GetSDKServer

func (s *Server) GetSDKServer() *mcp.Server

GetSDKServer returns the underlying MCP SDK server instance.

This is primarily for testing purposes to allow integration tests to connect the server with custom transports (e.g., in-memory).

Thread Safety:

Safe to call concurrently.

Example (for testing):

mcpServer := mcp.NewMCPServer(config, dt)
sdkServer := mcpServer.GetSDKServer()
session, _ := sdkServer.Connect(ctx, inMemoryTransport, nil)

Returns:

  • *mcp.Server: The MCP SDK server instance

func (*Server) GetStore

func (s *Server) GetStore() *devtools.Store

GetStore returns the Store instance.

This provides direct access to collected debug data for resource handlers. Used internally by resource/tool implementations (Task 2.x, 3.x).

Thread Safety:

Safe to call concurrently.

Example:

store := server.GetStore()
components := store.GetAllComponents()
fmt.Printf("Tracking %d components\n", len(components))

Returns:

  • *devtools.Store: The Store instance

func (*Server) RegisterClearEventLogTool

func (s *Server) RegisterClearEventLogTool() error

RegisterClearEventLogTool registers the clear_event_log tool with the MCP server.

This tool allows AI agents to clear the event log. It requires explicit confirmation to prevent accidental data loss.

The tool is registered with JSON Schema validation for parameters.

Thread Safety:

Safe to call concurrently. Uses MCP SDK's thread-safe registration.

Example:

server, _ := NewMCPServer(cfg, dt)
err := server.RegisterClearEventLogTool()
if err != nil {
    log.Fatalf("Failed to register clear event log tool: %v", err)
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterClearStateHistoryTool

func (s *Server) RegisterClearStateHistoryTool() error

RegisterClearStateHistoryTool registers the clear_state_history tool with the MCP server.

This tool allows AI agents to clear the state change history. It requires explicit confirmation to prevent accidental data loss.

The tool is registered with JSON Schema validation for parameters.

Thread Safety:

Safe to call concurrently. Uses MCP SDK's thread-safe registration.

Example:

server, _ := NewMCPServer(cfg, dt)
err := server.RegisterClearStateHistoryTool()
if err != nil {
    log.Fatalf("Failed to register clear state history tool: %v", err)
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterComponentResource

func (s *Server) RegisterComponentResource() error

RegisterComponentResource registers the individual component resource template.

This resource template provides access to individual components by ID via the URI pattern: bubblyui://components/{id}

The resource returns a ComponentSnapshot JSON structure for the requested component.

Thread Safety:

Safe to call concurrently. Resource reads use Store's thread-safe methods.

Example Request:

URI: bubblyui://components/comp-123

Example Response:

{
  "id": "comp-123",
  "name": "Counter",
  "type": "Counter",
  "state": {"count": 42},
  "refs": [...],
  "props": {...},
  "children": [...]
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterComponentsResource

func (s *Server) RegisterComponentsResource() error

RegisterComponentsResource registers the full component tree resource.

This resource provides access to all root components and their children via the URI: bubblyui://components

The resource returns a ComponentsResource JSON structure containing:

  • roots: Array of root component snapshots
  • total_count: Total number of components tracked
  • timestamp: When the snapshot was captured

Thread Safety:

Safe to call concurrently. Resource reads use Store's thread-safe methods.

Example Response:

{
  "roots": [
    {
      "id": "comp-1",
      "name": "App",
      "type": "App",
      "state": {},
      "refs": [],
      "props": {},
      "children": [...]
    }
  ],
  "total_count": 5,
  "timestamp": "2025-01-13T14:30:00Z"
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterEventsResource

func (s *Server) RegisterEventsResource() error

RegisterEventsResource registers the events resources.

This method registers two resources:

  • bubblyui://events/log - All event records
  • bubblyui://events/{id} - Individual event by ID

Thread Safety:

Safe to call concurrently. Resource reads use Store's thread-safe methods.

Example Response (bubblyui://events/log):

{
  "events": [
    {
      "seq_id": 1,
      "id": "event-1",
      "name": "click",
      "source_id": "comp-1",
      "target_id": "comp-2",
      "payload": {"button": "submit"},
      "timestamp": "2025-01-13T14:30:00Z",
      "duration": 5000000
    }
  ],
  "total_count": 1,
  "timestamp": "2025-01-13T14:30:00Z"
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterExportTool

func (s *Server) RegisterExportTool() error

RegisterExportTool registers the export_session tool with the MCP server.

This tool allows AI agents to export debug data with compression and sanitization. It supports multiple formats (JSON, YAML, MessagePack) and selective inclusion of data sections.

The tool is registered with JSON Schema validation for parameters.

Thread Safety:

Safe to call concurrently. Uses MCP SDK's thread-safe registration.

Example:

server, _ := NewMCPServer(cfg, dt)
err := server.RegisterExportTool()
if err != nil {
    log.Fatalf("Failed to register export tool: %v", err)
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterFilterEventsTool

func (s *Server) RegisterFilterEventsTool() error

RegisterFilterEventsTool registers the filter_events tool with the MCP server.

This tool allows AI agents to filter events by name, source, and time range. Results can be limited to prevent overwhelming responses.

The tool is registered with JSON Schema validation for parameters.

Thread Safety:

Safe to call concurrently. Uses MCP SDK's thread-safe registration.

Example:

server, _ := NewMCPServer(cfg, dt)
err := server.RegisterFilterEventsTool()
if err != nil {
    log.Fatalf("Failed to register filter events tool: %v", err)
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterPerformanceResource

func (s *Server) RegisterPerformanceResource() error

RegisterPerformanceResource registers the performance resources.

This method registers the following resource:

  • bubblyui://performance/metrics - All performance metrics with summary

Thread Safety:

Safe to call concurrently. Resource reads use Store's thread-safe methods.

Example Response (bubblyui://performance/metrics):

{
  "components": {
    "comp-1": {
      "component_id": "comp-1",
      "component_name": "Counter",
      "render_count": 42,
      "total_render_time": "210ms",
      "avg_render_time": "5ms",
      "max_render_time": "15ms",
      "min_render_time": "2ms",
      "memory_usage": 1024,
      "last_update": "2025-01-13T14:30:00Z"
    }
  },
  "summary": {
    "total_components": 3,
    "total_renders": 150,
    "slowest_component": "comp-2",
    "slowest_render_time": "50ms",
    "fastest_component": "comp-1",
    "fastest_render_time": "2ms",
    "most_rendered_component": "comp-1",
    "most_rendered_count": 42
  },
  "timestamp": "2025-01-13T14:30:00Z"
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterSearchComponentsTool

func (s *Server) RegisterSearchComponentsTool() error

RegisterSearchComponentsTool registers the search_components tool with the MCP server.

This tool allows AI agents to search for components by name, type, or ID with fuzzy matching support. Results are ranked by relevance.

The tool is registered with JSON Schema validation for parameters.

Thread Safety:

Safe to call concurrently. Uses MCP SDK's thread-safe registration.

Example:

server, _ := NewMCPServer(cfg, dt)
err := server.RegisterSearchComponentsTool()
if err != nil {
    log.Fatalf("Failed to register search components tool: %v", err)
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) RegisterSetRefValueTool

func (s *Server) RegisterSetRefValueTool() error

RegisterSetRefValueTool registers the set_ref_value tool with the MCP server.

This tool allows AI agents to modify ref values for testing purposes. It is a WRITE operation and only registers if WriteEnabled=true in Config.

The tool performs type checking to prevent invalid value assignments and supports dry-run mode for validation without side effects.

Thread Safety:

Safe to call concurrently. Uses MCP SDK's thread-safe registration.

Example:

server, _ := NewMCPServer(cfg, dt)
err := server.RegisterSetRefValueTool()
if err != nil {
    log.Fatalf("Failed to register set ref value tool: %v", err)
}

Returns:

  • error: nil on success, error if WriteEnabled=false or registration fails

func (*Server) RegisterStateResource

func (s *Server) RegisterStateResource() error

RegisterStateResource registers the state resources.

This method registers two resources:

  • bubblyui://state/refs - All reactive references
  • bubblyui://state/history - State change history

Thread Safety:

Safe to call concurrently. Resource reads use Store's thread-safe methods.

Example Response (bubblyui://state/refs):

{
  "refs": [
    {
      "id": "ref-1",
      "name": "count",
      "type": "int",
      "value": 42,
      "owner_id": "comp-1",
      "owner_name": "Counter",
      "watchers": 2
    }
  ],
  "computed": [],
  "timestamp": "2025-01-13T14:30:00Z"
}

Returns:

  • error: nil on success, error describing the failure otherwise

func (*Server) StartHTTPServer

func (s *Server) StartHTTPServer(ctx context.Context) error

StartHTTPServer starts the MCP server using HTTP/SSE transport.

This method enables IDE integration and remote debugging by serving MCP over HTTP with Server-Sent Events for real-time updates. The server will:

  • Create a StreamableHTTPHandler for MCP sessions
  • Set up HTTP endpoints (/mcp for MCP protocol, /health for health checks)
  • Listen on the configured host and port
  • Support multiple concurrent client connections
  • Handle graceful shutdown on context cancellation

The method starts the HTTP server in a goroutine and blocks until:

  • Context is canceled (graceful shutdown)
  • Server encounters a fatal error

Thread Safety:

Safe to call concurrently (uses internal mutex for state access).

Error Handling:

All errors are wrapped with context. Panics are recovered and reported
to the observability system. MCP failures never crash the host application.

Example:

dt := devtools.Enable()
cfg := mcp.DefaultMCPConfig()
cfg.Transport = mcp.MCPTransportHTTP
cfg.HTTPPort = 8765
cfg.HTTPHost = "localhost"

server, err := mcp.NewMCPServer(cfg, dt)
if err != nil {
    log.Fatal(err)
}

// Start HTTP server (blocks until context canceled)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

if err := server.StartHTTPServer(ctx); err != nil {
    log.Printf("HTTP server error: %v", err)
}

Parameters:

  • ctx: Context for cancellation and timeout control

Returns:

  • error: Configuration error, bind error, or nil on clean shutdown

func (*Server) StartStdioServer

func (s *Server) StartStdioServer(ctx context.Context) error

StartStdioServer starts the MCP server using stdio transport.

This method enables local CLI integration by communicating over stdin/stdout using newline-delimited JSON-RPC messages. The server will:

  • Create a stdio transport (uses os.Stdin/os.Stdout)
  • Connect to the MCP SDK server
  • Complete the initialization handshake with the client
  • Negotiate protocol version (2025-06-18)
  • Declare server capabilities (resources, tools, subscriptions)
  • Block until the client disconnects or context is canceled

The method blocks until one of the following occurs:

  • Client disconnects gracefully
  • Context is canceled
  • Transport error occurs

Thread Safety:

Safe to call concurrently (uses internal mutex for state access).

Error Handling:

All errors are wrapped with context. Panics are recovered and reported
to the observability system. MCP failures never crash the host application.

Example:

dt := devtools.Enable()
cfg := mcp.DefaultMCPConfig()
server, err := mcp.NewMCPServer(cfg, dt)
if err != nil {
    log.Fatal(err)
}

// Start stdio server (blocks until client disconnects)
ctx := context.Background()
if err := server.StartStdioServer(ctx); err != nil {
    log.Printf("Stdio server error: %v", err)
}

Parameters:

  • ctx: Context for cancellation and timeout control

Returns:

  • error: Connection error, session error, or nil on clean shutdown

type SetRefResult

type SetRefResult struct {
	// RefID is the ref that was modified
	RefID string `json:"ref_id"`

	// OldValue is the value before the change
	OldValue interface{} `json:"old_value"`

	// NewValue is the value after the change
	NewValue interface{} `json:"new_value"`

	// OwnerID is the component that owns this ref
	OwnerID string `json:"owner_id"`

	// Timestamp is when the operation was performed
	Timestamp time.Time `json:"timestamp"`

	// DryRun indicates if this was a validation-only operation
	DryRun bool `json:"dry_run"`

	// TypeMatch indicates if the new value type matches the old value type
	TypeMatch bool `json:"type_match"`
}

SetRefResult contains the result of a set_ref_value operation.

This structure is returned to AI agents after a successful (or dry-run) operation.

Example:

{
  "ref_id": "ref-counter-123",
  "old_value": 41,
  "new_value": 42,
  "owner_id": "comp-main-456",
  "timestamp": "2025-01-13T14:30:22Z",
  "dry_run": false,
  "type_match": true
}

type SetRefValueParams

type SetRefValueParams struct {
	// RefID is the unique identifier of the ref to modify
	RefID string `json:"ref_id"`

	// NewValue is the new value to set (type must match ref's current type)
	NewValue interface{} `json:"new_value"`

	// DryRun validates the operation without applying changes
	// Useful for checking if a value change would succeed
	DryRun bool `json:"dry_run"`
}

SetRefValueParams defines the parameters for the set_ref_value tool.

This structure is used by AI agents to modify ref values for testing purposes. This is a WRITE operation and requires WriteEnabled=true in Config.

Example:

{
  "ref_id": "ref-counter-123",
  "new_value": 42,
  "dry_run": false
}

type StateChangeDetector

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

StateChangeDetector hooks into DevTools to detect changes for subscriptions.

It monitors ref changes, component lifecycle events, and event emissions, then notifies subscribed clients when changes match their subscription filters.

Thread Safety:

All methods are thread-safe and can be called concurrently.

Lifecycle:

  1. NewStateChangeDetector() - Creates the detector
  2. Initialize() - Hooks into DevTools
  3. ... DevTools fires hooks as changes occur ...
  4. HandleRefChange/HandleComponentMount/HandleEventEmit - Process changes
  5. ... Notifications sent to subscribed clients ...

Example:

sm := NewSubscriptionManager(50)
detector := NewStateChangeDetector(sm)
err := detector.Initialize(devtools.Enable())
if err != nil {
    log.Printf("Failed to initialize detector: %v", err)
}

func NewStateChangeDetector

func NewStateChangeDetector(subscriptionMgr *SubscriptionManager) *StateChangeDetector

NewStateChangeDetector creates a new state change detector.

The detector is created but not yet hooked into DevTools. Call Initialize() to register the hooks.

Thread Safety:

Safe to call concurrently (creates new instance each time).

Example:

sm := NewSubscriptionManager(50)
detector := NewStateChangeDetector(sm)

Parameters:

  • subscriptionMgr: The subscription manager to use for tracking subscriptions

Returns:

  • *StateChangeDetector: A new change detector instance

func (*StateChangeDetector) HandleComponentMount

func (d *StateChangeDetector) HandleComponentMount(componentID, componentName string)

HandleComponentMount processes a component mount event and notifies subscribed clients.

This method is called when a component is mounted in the application. It checks all active subscriptions for the components resource and notifies clients whose filters match the mounted component.

The notification includes:

  • component_id: Unique identifier of the mounted component
  • component_name: Name/type of the mounted component
  • event_type: "mount"

Thread Safety:

This method acquires a read lock and is safe for concurrent use.

Parameters:

  • componentID: Unique identifier of the mounted component
  • componentName: Name/type of the mounted component

Example:

detector.HandleComponentMount("btn-123", "Button")

func (*StateChangeDetector) HandleComponentUnmount

func (d *StateChangeDetector) HandleComponentUnmount(componentID, componentName string)

HandleComponentUnmount processes a component unmount event and notifies subscribed clients.

This method:

  • Finds all subscriptions interested in component changes
  • Checks if the unmount event matches each subscription's filters
  • Queues notifications for matching subscriptions

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

detector.HandleComponentUnmount("comp-1", "Counter")

Parameters:

  • componentID: The unique identifier of the unmounted component
  • componentName: The name of the unmounted component

func (*StateChangeDetector) HandleEventEmit

func (d *StateChangeDetector) HandleEventEmit(eventName, componentID string, data interface{})

HandleEventEmit processes an event emission and notifies subscribed clients.

This method:

  • Finds all subscriptions interested in event emissions
  • Checks if the event matches each subscription's filters
  • Queues notifications for matching subscriptions

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

detector.HandleEventEmit("increment", "comp-1", nil)

Parameters:

  • eventName: The name of the emitted event
  • componentID: The ID of the component that emitted the event
  • data: Optional event data

func (*StateChangeDetector) HandleRefChange

func (d *StateChangeDetector) HandleRefChange(refID string, oldValue, newValue interface{})

HandleRefChange processes a ref value change and notifies subscribed clients.

This method:

  • Finds all subscriptions interested in ref changes
  • Checks if the change matches each subscription's filters
  • Queues notifications for matching subscriptions

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

detector.HandleRefChange("ref-1", 41, 42)

Parameters:

  • refID: The unique identifier of the ref that changed
  • oldValue: The value before the change
  • newValue: The value after the change

func (*StateChangeDetector) Initialize

func (d *StateChangeDetector) Initialize(dt *devtools.DevTools) error

Initialize hooks the detector into the DevTools system.

This method registers a custom hook with DevTools that will be called whenever state changes, components mount/unmount, or events are emitted.

Thread Safety:

Safe to call concurrently, but should only be called once per detector.

Example:

dt := devtools.Enable()
err := detector.Initialize(dt)
if err != nil {
    log.Printf("Failed to initialize: %v", err)
}

Parameters:

  • dt: The DevTools instance to hook into

Returns:

  • error: Initialization error, or nil on success

func (*StateChangeDetector) SetNotifier

func (d *StateChangeDetector) SetNotifier(notifier notificationSender)

SetNotifier sets the notification sender for this detector.

This method configures the detector to use the provided notifier for sending notifications to clients when changes are detected.

Thread Safety:

Safe to call concurrently, but should be called before any changes occur.

Example:

notifier, err := NewNotificationSender(batcher)
if err != nil {
    log.Fatal(err)
}
detector.SetNotifier(notifier)

Parameters:

  • notifier: The notification sender to use

type StateResource

type StateResource struct {
	// Refs contains all reactive references across all components
	Refs []*RefInfo `json:"refs"`

	// Computed contains all computed values (future enhancement)
	Computed []*ComputedInfo `json:"computed"`

	// Timestamp indicates when this snapshot was captured
	Timestamp time.Time `json:"timestamp"`
}

StateResource represents the reactive state resource.

This structure is returned by the bubblyui://state/refs resource and provides a snapshot of all reactive state in the application.

JSON Schema:

{
  "refs": [RefInfo],
  "computed": [ComputedInfo],
  "timestamp": string (ISO 8601)
}

type Subscription

type Subscription struct {
	// ID is the unique identifier for this subscription
	ID string

	// ClientID identifies the client that owns this subscription
	ClientID string

	// ResourceURI is the MCP resource URI being subscribed to
	// Examples: "bubblyui://components", "bubblyui://state/refs"
	ResourceURI string

	// Filters are optional criteria for filtering updates
	// Keys and values depend on the resource type
	// Example: {"ref_id": "count-ref"} to only receive updates for a specific ref
	Filters map[string]interface{}

	// CreatedAt is when the subscription was created
	CreatedAt time.Time
}

Subscription represents a client's subscription to a resource URI.

Subscriptions enable real-time updates when the subscribed resource changes. Each subscription is identified by a unique ID and associated with a client.

Thread Safety:

Subscription instances are immutable after creation.

Example:

sub := &Subscription{
    ID:         "sub-123",
    ClientID:   "client-456",
    ResourceURI: "bubblyui://state/refs",
    Filters:    map[string]interface{}{"ref_id": "count-ref"},
    CreatedAt:  time.Now(),
}

type SubscriptionManager

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

SubscriptionManager manages client subscriptions to MCP resources.

It maintains a registry of active subscriptions, enforces limits, prevents duplicates, and provides cleanup on client disconnect.

Thread Safety:

All methods are thread-safe and can be called concurrently.

Lifecycle:

  1. NewSubscriptionManager() - Creates the manager
  2. Subscribe() - Clients add subscriptions
  3. ... updates are sent via change detectors (Task 4.2) ...
  4. Unsubscribe() or UnsubscribeAll() - Cleanup

Example:

sm := NewSubscriptionManager(50) // Max 50 subscriptions per client
err := sm.Subscribe("client-1", "bubblyui://state/refs", nil)
if err != nil {
    log.Printf("Subscribe failed: %v", err)
}

func NewSubscriptionManager

func NewSubscriptionManager(maxPerClient int) *SubscriptionManager

NewSubscriptionManager creates a new subscription manager.

The maxPerClient parameter sets the maximum number of subscriptions a single client can have. This prevents resource exhaustion from subscription spam.

Thread Safety:

Safe to call concurrently (creates new instance each time).

Example:

sm := NewSubscriptionManager(50)

Parameters:

  • maxPerClient: Maximum subscriptions per client (typically 50)

Returns:

  • *SubscriptionManager: A new subscription manager instance

func (*SubscriptionManager) GetSubscriptionCount

func (sm *SubscriptionManager) GetSubscriptionCount(clientID string) int

GetSubscriptionCount returns the total number of subscriptions for a client.

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

count := sm.GetSubscriptionCount("client-1")
fmt.Printf("Client has %d subscriptions\n", count)

Parameters:

  • clientID: Unique identifier for the client

Returns:

  • int: Number of active subscriptions (0 if client not found)

func (*SubscriptionManager) GetSubscriptions

func (sm *SubscriptionManager) GetSubscriptions(clientID string) []*Subscription

GetSubscriptions returns all subscriptions for a client.

This method returns a copy of the subscriptions slice to prevent external modification of the internal state.

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

subs := sm.GetSubscriptions("client-1")
fmt.Printf("Client has %d subscriptions\n", len(subs))

Parameters:

  • clientID: Unique identifier for the client

Returns:

  • []*Subscription: Copy of client's subscriptions (empty slice if none)

func (*SubscriptionManager) Subscribe

func (sm *SubscriptionManager) Subscribe(clientID, uri string, filters map[string]interface{}) error

Subscribe adds a new subscription for a client.

This method:

  • Validates the subscription doesn't already exist (duplicate prevention)
  • Enforces the per-client subscription limit
  • Generates a unique subscription ID
  • Adds the subscription to the registry

Duplicate Detection:

A subscription is considered duplicate if the same client is already
subscribed to the same resource URI with the same filters.

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

err := sm.Subscribe("client-1", "bubblyui://state/refs", map[string]interface{}{
    "ref_id": "count-ref",
})
if err != nil {
    log.Printf("Subscribe failed: %v", err)
}

Parameters:

  • clientID: Unique identifier for the client
  • uri: MCP resource URI to subscribe to
  • filters: Optional filters for updates (can be nil)

Returns:

  • error: Validation error, limit exceeded, or nil on success

func (*SubscriptionManager) Unsubscribe

func (sm *SubscriptionManager) Unsubscribe(clientID, subscriptionID string) error

Unsubscribe removes a specific subscription.

This method:

  • Finds the subscription by ID
  • Removes it from the client's subscription list
  • Cleans up empty client entries

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

err := sm.Unsubscribe("client-1", "sub-123")
if err != nil {
    log.Printf("Unsubscribe failed: %v", err)
}

Parameters:

  • clientID: Unique identifier for the client
  • subscriptionID: ID of the subscription to remove

Returns:

  • error: Subscription not found, or nil on success

func (*SubscriptionManager) UnsubscribeAll

func (sm *SubscriptionManager) UnsubscribeAll(clientID string) error

UnsubscribeAll removes all subscriptions for a client.

This method is typically called when a client disconnects. It performs bulk cleanup of all the client's subscriptions.

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

err := sm.UnsubscribeAll("client-1")
if err != nil {
    log.Printf("UnsubscribeAll failed: %v", err)
}

Parameters:

  • clientID: Unique identifier for the client

Returns:

  • error: Client not found, or nil on success

type Throttler

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

Throttler prevents sending updates too frequently to clients.

It enforces a minimum interval between updates for each client+resource combination. This prevents overwhelming clients with high-frequency changes.

Thread Safety:

All methods are thread-safe and can be called concurrently.

Lifecycle:

  1. NewThrottler() - Creates the throttler
  2. ShouldSend() - Check if update should be sent
  3. ... automatic throttling occurs ...
  4. Reset() - Optional: reset throttle state for a client

Example:

throttler, err := NewThrottler(100*time.Millisecond)
if err != nil {
    log.Fatal(err)
}
if throttler.ShouldSend("client-1", "bubblyui://state/refs") {
    // Send update
}

func NewThrottler

func NewThrottler(minInterval time.Duration) (*Throttler, error)

NewThrottler creates a new throttler.

The throttler will enforce a minimum interval between updates for each client+resource combination.

Thread Safety:

Safe to call concurrently (creates new instance each time).

Example:

throttler, err := NewThrottler(100*time.Millisecond)
if err != nil {
    log.Fatal(err)
}

Parameters:

  • minInterval: Minimum time between updates (must be > 0)

Returns:

  • *Throttler: A new throttler instance
  • error: Validation error, or nil on success

func (*Throttler) Reset

func (t *Throttler) Reset(clientID string)

Reset clears the throttle state for a client.

This allows the next update to be sent immediately, regardless of when the last update was sent.

Typically called when a client disconnects or reconnects.

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

throttler.Reset("client-1")

Parameters:

  • clientID: The client to reset throttle state for

func (*Throttler) ShouldSend

func (t *Throttler) ShouldSend(clientID, resourceURI string) bool

ShouldSend checks if an update should be sent to a client.

Returns true if:

  • This is the first update for this client+resource
  • minInterval has elapsed since the last update

Returns false if:

  • minInterval has not yet elapsed (throttled)

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

if throttler.ShouldSend("client-1", "bubblyui://state/refs") {
    // Send update
    server.SendNotification(clientID, update)
}

Parameters:

  • clientID: The client to check
  • resourceURI: The resource URI to check

Returns:

  • bool: True if update should be sent, false if throttled

type TransportType

type TransportType int

TransportType defines the transport mechanism for MCP server.

Multiple transports can be enabled simultaneously using bitwise OR. For example: MCPTransportStdio | MCPTransportHTTP enables both.

const (
	// MCPTransportStdio enables stdio transport for local CLI integration
	MCPTransportStdio TransportType = 1 << iota

	// MCPTransportHTTP enables HTTP/SSE transport for IDE integration
	MCPTransportHTTP
)

func (TransportType) String

func (t TransportType) String() string

String returns the string representation of the transport type.

Returns:

  • string: Human-readable transport type (e.g., "stdio", "http", "stdio|http")

type UpdateBatcher

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

UpdateBatcher batches updates and flushes them periodically or when batch size is reached.

It collects updates for each client and flushes them either:

  • After flushInterval has elapsed since the last flush
  • When the batch reaches maxBatchSize updates

This prevents overwhelming clients with high-frequency updates.

Thread Safety:

All methods are thread-safe and can be called concurrently.

Lifecycle:

  1. NewUpdateBatcher() - Creates the batcher
  2. SetFlushHandler() - Configures the flush handler
  3. AddUpdate() - Add updates to batch
  4. ... automatic flushing occurs ...
  5. Stop() - Graceful shutdown (flushes pending updates)

Example:

batcher, err := NewUpdateBatcher(100*time.Millisecond, 10)
if err != nil {
    log.Fatal(err)
}
batcher.SetFlushHandler(func(clientID string, updates []UpdateNotification) {
    // Send updates to client
})
batcher.AddUpdate(notification)
defer batcher.Stop()

func NewUpdateBatcher

func NewUpdateBatcher(flushInterval time.Duration, maxBatchSize int) (*UpdateBatcher, error)

NewUpdateBatcher creates a new update batcher.

The batcher will flush updates either:

  • After flushInterval has elapsed
  • When a client's batch reaches maxBatchSize

Thread Safety:

Safe to call concurrently (creates new instance each time).

Example:

batcher, err := NewUpdateBatcher(100*time.Millisecond, 10)
if err != nil {
    log.Fatal(err)
}

Parameters:

  • flushInterval: Maximum time between flushes (must be > 0)
  • maxBatchSize: Maximum updates per batch (must be > 0)

Returns:

  • *UpdateBatcher: A new batcher instance
  • error: Validation error, or nil on success

func (*UpdateBatcher) AddUpdate

func (b *UpdateBatcher) AddUpdate(update UpdateNotification)

AddUpdate adds an update to the batch.

The update will be flushed either:

  • After flushInterval has elapsed
  • When the client's batch reaches maxBatchSize (immediate flush)

Thread Safety:

Safe to call concurrently from multiple goroutines.

Example:

update := UpdateNotification{
    ClientID: "client-1",
    URI:      "bubblyui://state/refs",
    Data:     map[string]interface{}{"ref_id": "ref-1", "value": 42},
}
batcher.AddUpdate(update)

Parameters:

  • update: The notification to add to the batch

func (*UpdateBatcher) SetFlushHandler

func (b *UpdateBatcher) SetFlushHandler(handler FlushHandler)

SetFlushHandler sets the handler to call when flushing updates.

This must be called before adding any updates.

Thread Safety:

Not safe to call concurrently with AddUpdate().
Should be called once during initialization.

Example:

batcher.SetFlushHandler(func(clientID string, updates []UpdateNotification) {
    for _, update := range updates {
        server.SendNotification(clientID, update)
    }
})

Parameters:

  • handler: The flush handler function

func (*UpdateBatcher) Stop

func (b *UpdateBatcher) Stop()

Stop gracefully shuts down the batcher.

This method:

  • Stops the flush timer
  • Flushes all pending updates
  • Waits for the flush goroutine to complete

Thread Safety:

Safe to call concurrently, but should only be called once.

Example:

defer batcher.Stop()

After calling Stop(), no more updates should be added.

type UpdateNotification

type UpdateNotification struct {
	// ClientID identifies the client to send the notification to
	ClientID string

	// URI is the resource URI that changed
	URI string

	// Data contains the notification payload
	Data map[string]interface{}
}

UpdateNotification represents a notification to be sent to a client.

Notifications are batched and throttled to prevent client overload.

Thread Safety:

UpdateNotification instances are immutable after creation.

Example:

notification := UpdateNotification{
    ClientID: "client-1",
    URI:      "bubblyui://state/refs",
    Data:     map[string]interface{}{"ref_id": "ref-1", "value": 42},
}

Jump to

Keyboard shortcuts

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