@@ -14,10 +14,19 @@ import (
1414 "github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
1515)
1616
17+ // defaultSearchPageSize is the page size used when paginating full-coverage
18+ // scans (GetToolsByServer, DeleteAll). It is a generous upper bound on the
19+ // number of docs in a single page; pagination loops over as many pages as
20+ // needed, so total coverage is never bounded by this value (MCP-3319).
21+ const defaultSearchPageSize = 10000
22+
1723// BleveIndex wraps Bleve index operations
1824type BleveIndex struct {
1925 index bleve.Index
2026 logger * zap.Logger
27+ // searchPageSize bounds a single search page during paginated full scans.
28+ // Defaults to defaultSearchPageSize; overridable in tests.
29+ searchPageSize int
2130}
2231
2332// ToolDocument represents a tool document in the index
@@ -61,8 +70,9 @@ func newBleveIndexAt(indexPath string, logger *zap.Logger) (*BleveIndex, error)
6170 }
6271
6372 return & BleveIndex {
64- index : index ,
65- logger : logger ,
73+ index : index ,
74+ logger : logger ,
75+ searchPageSize : defaultSearchPageSize ,
6676 }, nil
6777}
6878
@@ -304,21 +314,29 @@ func (b *BleveIndex) GetDocumentCount() (uint64, error) {
304314// its on-disk directory.
305315func (b * BleveIndex ) DeleteAll () error {
306316 query := bleve .NewMatchAllQuery ()
307- searchReq := bleve .NewSearchRequest (query )
308- searchReq .Size = 100000 // generous upper bound on indexed tools
309- searchResult , err := b .index .Search (searchReq )
310- if err != nil {
311- return fmt .Errorf ("failed to enumerate documents for delete-all: %w" , err )
312- }
317+ // Delete in pages until the index is empty. Each iteration enumerates a
318+ // page from offset 0 and deletes exactly those docs, so the next search
319+ // surfaces the following page — no From offset bookkeeping, and coverage is
320+ // not bounded by a single search page (MCP-3319).
321+ for {
322+ searchReq := bleve .NewSearchRequest (query )
323+ searchReq .Size = b .searchPageSize
324+ searchResult , err := b .index .Search (searchReq )
325+ if err != nil {
326+ return fmt .Errorf ("failed to enumerate documents for delete-all: %w" , err )
327+ }
328+ if len (searchResult .Hits ) == 0 {
329+ return nil
330+ }
313331
314- batch := b .index .NewBatch ()
315- for _ , hit := range searchResult .Hits {
316- batch .Delete (hit .ID )
317- }
318- if batch .Size () == 0 {
319- return nil
332+ batch := b .index .NewBatch ()
333+ for _ , hit := range searchResult .Hits {
334+ batch .Delete (hit .ID )
335+ }
336+ if err := b .index .Batch (batch ); err != nil {
337+ return fmt .Errorf ("failed to delete documents for delete-all: %w" , err )
338+ }
320339 }
321- return b .index .Batch (batch )
322340}
323341
324342// Batch operations for efficiency
@@ -382,30 +400,40 @@ func (b *BleveIndex) GetToolsByServer(serverName string) ([]*config.ToolMetadata
382400 query := bleve .NewTermQuery (serverName )
383401 query .SetField ("server_name" )
384402
385- // Create search request with high limit to get all tools
386- searchReq := bleve .NewSearchRequest (query )
387- searchReq .Size = 10000 // Maximum tools per server
388- searchReq .Fields = []string {"tool_name" , "full_tool_name" , "server_name" , "description" , "params_json" , "output_schema_json" , "hash" }
403+ fields := []string {"tool_name" , "full_tool_name" , "server_name" , "description" , "params_json" , "output_schema_json" , "hash" }
389404
390405 b .logger .Debug ("Querying tools by server" , zap .String ("server" , serverName ))
391406
392- searchResult , err := b .index .Search (searchReq )
393- if err != nil {
394- return nil , fmt .Errorf ("failed to query tools by server: %w" , err )
395- }
396-
397- // Convert results to ToolMetadata
407+ // Paginate so a server exposing more than one search page of tools is fully
408+ // covered — a single capped search would silently drop the overflow
409+ // (MCP-3319). A short final page (fewer hits than the page size) ends the loop.
398410 var tools []* config.ToolMetadata
399- for _ , hit := range searchResult .Hits {
400- toolMeta := & config.ToolMetadata {
401- Name : getStringField (hit .Fields , "full_tool_name" ),
402- ServerName : getStringField (hit .Fields , "server_name" ),
403- Description : getStringField (hit .Fields , "description" ),
404- ParamsJSON : getStringField (hit .Fields , "params_json" ),
405- OutputSchemaJSON : getStringField (hit .Fields , "output_schema_json" ),
406- Hash : getStringField (hit .Fields , "hash" ),
411+ for from := 0 ; ; from += b .searchPageSize {
412+ searchReq := bleve .NewSearchRequest (query )
413+ searchReq .From = from
414+ searchReq .Size = b .searchPageSize
415+ searchReq .Fields = fields
416+
417+ searchResult , err := b .index .Search (searchReq )
418+ if err != nil {
419+ return nil , fmt .Errorf ("failed to query tools by server: %w" , err )
420+ }
421+
422+ for _ , hit := range searchResult .Hits {
423+ toolMeta := & config.ToolMetadata {
424+ Name : getStringField (hit .Fields , "full_tool_name" ),
425+ ServerName : getStringField (hit .Fields , "server_name" ),
426+ Description : getStringField (hit .Fields , "description" ),
427+ ParamsJSON : getStringField (hit .Fields , "params_json" ),
428+ OutputSchemaJSON : getStringField (hit .Fields , "output_schema_json" ),
429+ Hash : getStringField (hit .Fields , "hash" ),
430+ }
431+ tools = append (tools , toolMeta )
432+ }
433+
434+ if len (searchResult .Hits ) < b .searchPageSize {
435+ break
407436 }
408- tools = append (tools , toolMeta )
409437 }
410438
411439 b .logger .Debug ("Found tools for server" ,
0 commit comments