mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-06-05 22:23:50 +00:00
5ad975ee7a
* v2.0: token-optimization overhaul Additive (backward-compatible flags): - file_read: skeleton mode, strip (imports/license/block_comments), compact_line_numbers, 8-char etag with prefix-match compat - ast_query: format=verbose|compact|location, pagination cursor - file_search: cluster mode, pagination cursor - lsp_query (references): compact output Breaking (v2): - Preambles removed; opt-in verbose=true restores - edit_apply: response=count|diff|none, default count - ping tool removed - symbol_at/find_definition/find_references merged into lsp_query - Tool descriptions trimmed -83%, help moved to filepuff://help/<tool> - Batch file_read dedups by etag Protocol: - ResourceLink returned for file_read >64 KiB (force_inline override) - OnAfterInitialize hook reads capabilities.experimental.filepuff for session defaults (default_format, default_max_results, default_cluster, compact_refs, line_numbers, resource_link_threshold) * fix: drop --max-total-count from ripgrep args The flag does not exist in stable ripgrep (confirmed up to 15.1.0 -- "unrecognized flag --max-total-count, similar flags that are available: --max-count"). Every file_search call failed on hosts with stock rg. --max-count is per-file, not a drop-in replacement, so rely on the in-process truncation in parseOutput that was already the documented safety net.
146 lines
4.2 KiB
Go
146 lines
4.2 KiB
Go
// Package server implements the MCP server for file operations.
|
|
package server
|
|
|
|
import (
|
|
"sync/atomic"
|
|
"unsafe"
|
|
)
|
|
|
|
// SessionPrefs holds client-declared session-wide preferences parsed from
|
|
// InitializeRequest.Params.Capabilities.Experimental["filepuff"].
|
|
//
|
|
// These act as defaults; explicit per-call flags always override them.
|
|
//
|
|
// Supported keys in the "filepuff" experimental map:
|
|
//
|
|
// terse bool — no-op (v2 default is already terse; reserved for future)
|
|
// default_format string — ast_query format default ("verbose"|"compact"|"location")
|
|
// default_max_results int — file_search and ast_query max_results when not supplied
|
|
// default_cluster bool — file_search cluster default
|
|
// compact_refs bool — lsp_query references compact default
|
|
// line_numbers string — file_read line prefix default ("none"|"compact"|"full")
|
|
// resource_link_threshold int — per-session override for cfg.ResourceLinkThresholdBytes
|
|
type SessionPrefs struct {
|
|
// ASTQueryFormat is the default format for ast_query ("verbose", "compact", "location").
|
|
// Empty string means "use handler built-in default".
|
|
ASTQueryFormat string
|
|
|
|
// DefaultMaxResults is the default max_results for file_search and ast_query.
|
|
// 0 means "use handler built-in default".
|
|
DefaultMaxResults int
|
|
|
|
// DefaultCluster is the default cluster flag for file_search.
|
|
// nil means "use handler built-in default (false)".
|
|
DefaultCluster *bool
|
|
|
|
// CompactRefs is the default compact flag for lsp_query action=references.
|
|
// nil means "use handler built-in default (false)".
|
|
CompactRefs *bool
|
|
|
|
// LineNumbers controls the file_read line prefix default.
|
|
// "" = use handler built-in default (full).
|
|
// "none" = no line numbers.
|
|
// "compact" = compact prefix (N│content).
|
|
// "full" = standard padded prefix ( N│ content).
|
|
LineNumbers string
|
|
|
|
// ResourceLinkThreshold overrides cfg.ResourceLinkThresholdBytes for this session.
|
|
// 0 means "use config default".
|
|
ResourceLinkThreshold int
|
|
}
|
|
|
|
// boolPtr returns a pointer to a bool value.
|
|
func boolPtr(b bool) *bool { return &b }
|
|
|
|
// ParseSessionPrefs parses the raw map from
|
|
// InitializeRequest.Params.Capabilities.Experimental["filepuff"].
|
|
// Unknown keys are silently ignored. Type mismatches for individual keys are
|
|
// silently ignored (key is treated as absent). Returns zero-value SessionPrefs
|
|
// when raw is nil or empty — callers should treat zero values as "use built-in defaults".
|
|
func ParseSessionPrefs(raw map[string]any) SessionPrefs {
|
|
if len(raw) == 0 {
|
|
return SessionPrefs{}
|
|
}
|
|
|
|
var p SessionPrefs
|
|
|
|
if v, ok := raw["default_format"]; ok {
|
|
if s, ok := v.(string); ok {
|
|
switch s {
|
|
case "verbose", "compact", "location":
|
|
p.ASTQueryFormat = s
|
|
}
|
|
}
|
|
}
|
|
|
|
if v, ok := raw["default_max_results"]; ok {
|
|
if n := toInt(v); n > 0 {
|
|
p.DefaultMaxResults = n
|
|
}
|
|
}
|
|
|
|
if v, ok := raw["default_cluster"]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
p.DefaultCluster = boolPtr(b)
|
|
}
|
|
}
|
|
|
|
if v, ok := raw["compact_refs"]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
p.CompactRefs = boolPtr(b)
|
|
}
|
|
}
|
|
|
|
if v, ok := raw["line_numbers"]; ok {
|
|
if s, ok := v.(string); ok {
|
|
switch s {
|
|
case "none", "compact", "full":
|
|
p.LineNumbers = s
|
|
}
|
|
}
|
|
}
|
|
|
|
if v, ok := raw["resource_link_threshold"]; ok {
|
|
if n := toInt(v); n >= 0 {
|
|
p.ResourceLinkThreshold = n
|
|
}
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
// toInt converts numeric JSON-decoded values (float64, int, int64) to int.
|
|
// Returns 0 for unsupported types or negative values.
|
|
func toInt(v any) int {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
if n >= 0 {
|
|
return int(n)
|
|
}
|
|
case int:
|
|
if n >= 0 {
|
|
return n
|
|
}
|
|
case int64:
|
|
if n >= 0 {
|
|
return int(n)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// sessionPrefsPtr is an atomic pointer helper for thread-safe access to *SessionPrefs.
|
|
// We use atomic store/load so the hook (called once at init) and handlers (many goroutines)
|
|
// never race. The prefs are write-once after initialization.
|
|
type sessionPrefsPtr struct {
|
|
p unsafe.Pointer // *SessionPrefs
|
|
}
|
|
|
|
func (sp *sessionPrefsPtr) Store(prefs *SessionPrefs) {
|
|
atomic.StorePointer(&sp.p, unsafe.Pointer(prefs))
|
|
}
|
|
|
|
func (sp *sessionPrefsPtr) Load() *SessionPrefs {
|
|
return (*SessionPrefs)(atomic.LoadPointer(&sp.p))
|
|
}
|