An answer about your documents is only as good as the text it was built from, and when you paste whole files into a chat, you cannot tell which part the model actually used. GroupDocs.Search.Mcp, running locally in Docker, gives Claude Desktop, Cursor or Copilot a different primitive: get_text_fragments returns only the passages that match a query, with a few words of context and the file they came from, so the agent answers from quotes:

What do my contracts say about penalties? Quote the passages, with enough context to understand each one.

The step-by-step version with config and troubleshooting is in the documentation: How to get the matching passages instead of reading whole files.

Why do answers drift from the documents?

Because the context window is a budget, and whole documents spend it badly. A question about penalties needs perhaps three sentences out of fifty pages. Paste the fifty pages and the model has to find the three sentences itself, among everything else, and then hold them while it writes. Long contexts get summarised, early material loses weight, and the answer starts to rest on the model’s general sense of what contracts usually say rather than on what yours say. The industry calls the discipline of managing this context engineering; the simplest form of it is not loading text you do not need.

What does a fragment look like?

The index already holds every document’s text, so the server can cut out the passage around each match. These are real responses from our test, against five synthetic contracts. The defaults, eight words either side, give a phrase:

-> get_text_fragments {"query":"penalty","indexName":"contracts"}
<- === contracts/acme-master-services-agreement.pdf (1 occurrence(s)) ===
     [content] …before the end of the Term, a termination penalty of
   three monthly fees applies, payable within 30…

Twenty words either side give the whole clause:

-> get_text_fragments {"query":"penalty","indexName":"contracts",
     "termsBefore":20,"termsAfter":20,"maxFragmentsPerDocument":1}
<- === contracts/acme-master-services-agreement.pdf (1 occurrence(s)) ===
     …convenience on 90 days written notice.
   12.3 If the Client terminates before the end of the Term, a termination
   penalty of three monthly fees applies, payable within 30 days of the
   termination date.
   14.2 Force majeure: neither party is…

termsBefore and termsAfter are a dial: small for “which files and roughly where”, large for “quote me the clause”. maxFragmentsPerDocument and maxDocuments cap the size of the answer, so a word that occurs two hundred times does not flood the context.

The budget, side by side

Reading the files                        Searching the index
-------------------------------------    -------------------------------------
agent opens every document               search: which files match (JSON)
  context grows with the corpus          get_text_fragments: the passages
  long files crowd out the question        (~8 words either side, per file)
  repeated for every question            answer cites file + passage
                                         the index is reused next time

The difference grows with the corpus. For three short files it hardly matters. For a folder of long contracts, reports and mail, the fragments for one question are a few hundred words, against the full text of every candidate document.

When the words differ

Full-text search matches words, which is a feature for grounding (every hit is explainable) and a limit for recall (a synonym is a miss). The agent’s job is to try variants, and it can see what each variant actually matched:

-> search {"query":"renewal OR renews","indexName":"contracts"}
<- umbrella-office-lease.pdf, terms ["renewal", "renews"]

-> search {"query":"renewl","indexName":"contracts","fuzzy":true}
<- umbrella-office-lease.pdf, terms ["renewal", "renews", "rent"]

Fuzzy matching rescued a misspelling and also pulled in rent. Because the response lists the terms it matched, an agent, or you, can see the over-match instead of trusting it. That transparency is what “grounded” should mean: not only “the answer cites a source”, but “you can see how the source was found”.

What a grounded answer looks like

Ask for quotes, and the answer arrives in a shape you can verify at a glance:

Two contracts carry a termination penalty. Acme MSA, clause 12.3: “a termination penalty of three monthly fees applies, payable within 30 days of the termination date.” Globex consulting agreement, section 9: “the Client pays 50 percent of the fees remaining for the current phase.” The Initech NDA mentions termination and penalty but states there is no penalty.

If a summary misreads a quote, the misreading sits right next to the quote.

Honest limits

  • In evaluation mode an index holds at most 100 documents and a search returns at most 10. Evaluation mode also adds an “Evaluation only…” line to the indexed text of some formats, which can show up in fragments; license the server and rebuild the index to drop it. Call get_license_status first.
  • Fragment context is counted in words, not sentences, so a passage can start mid-sentence; raise termsBefore when it matters.
  • No OCR: scans without a text layer have no passages to return.
  • Docker only; on Linux add --user "$(id -u):$(id -g)".

FAQ

How do I make Claude cite sources from my documents? Give it a tool that returns the source text with the file name. get_text_fragments does exactly that, and an instruction like “quote the passage and its file” makes it a habit.

Does this reduce token usage? It avoids loading whole documents to answer a question: the agent reads the matching passages only. How much that saves depends on the size of your files.

Can the agent still read a whole file when it needs to? Yes, with its own file tools. Fragments are for finding and quoting; read the whole file when the passage is not enough.

Go deeper