adityamparikh opened a new issue, #208:
URL: https://github.com/apache/solr-mcp/issues/208

   ## Summary
   
   Add an `index-url` MCP tool that streams a JSON, CSV, XML or Markdown 
document set from an http(s) URL into a Solr collection, so the payload never 
passes through the model's context. It is registered in **both** the STDIO and 
HTTP transports. This issue records the design and the decisions behind it 
before any code is written; the full implementation spec (tool contract, fetch 
algorithm, error messages, test list, threat-model edits) will accompany the PR.
   
   ## Problem
   
   All four indexing tools (`index-json-documents`, `index-csv-documents`, 
`index-xml-documents`, `index-markdown-documents`) take the payload as a 
tool-call argument, so the model has to emit every byte.
   
   Measured when #197 was closed: indexing the 61-record `shows.json` sample 
through a live server took over two minutes. Solr and the server accounted for 
under one second. The rest was the model emitting ~9,500 tokens of escaped 
JSON. Anything larger than one response's output budget has to be split across 
calls, and each call commits, so a large file lands as a partial, multi-commit 
ingest with no transactional boundary.
   
   A URL argument is ~20 tokens regardless of payload size.
   
   ## Why a URL and not a file upload
   
   MCP has no client-to-server file transfer. SEP-2356 (declarative file 
inputs) was closed in favour of SEP-2631 ("File Objects and Transfer"), which 
is still an open draft. A file attached in Claude Desktop is extracted into the 
model's context; the model gets content and a filename, never a path or handle 
it can pass to a server. A URL is the only non-inline source both transports 
can reach identically today.
   
   #194 added a STDIO-only `index-file` tool and was closed because it "would 
give the two transports different tool surfaces for the same job". `index-url` 
gives HTTP a non-inline bulk path, which is what that objection asked for.
   
   ## Proposed design
   
   One new `@Service`, `UrlIndexingService`, with no `@Profile` gate:
   
   ```
   index-url(collection: String, url: String, format: String?) -> summary string
   ```
   
   It fetches the URL with the JDK `HttpClient`, wraps the body in a `Reader`, 
and hands it to the same streaming spine that `index-file` uses 
(`IndexingDocumentCreator.stream(Reader, format, consumer)`, batches of 1,000, 
commit at the end). The parsers never learn where the bytes came from, so this 
is a second adapter over already-tested code, not a new pipeline.
   
   ```
     INLINE  index-{json,csv,xml,markdown}-documents(collection, payload)  --> 
List<SolrInputDocument>
                                                                                
      |
     URL     index-url(collection, url, format?)   HttpClient -> 
InputStreamReader ---+--> stream(Reader) --> batches --> Solr
                                                                                
      |
     FILE    index-file(collection, path, format?) Files.newBufferedReader 
-----------+   (STDIO only)
   ```
   
   Fetch behaviour:
   
   - **Format resolution:** explicit `format` > URL path extension (query 
string stripped) > `Content-Type` > error. Extension outranks the header 
because `raw.githubusercontent.com` serves `.json` as `text/plain`. 
`text/plain` and `text/html` map to nothing; the error suggests `format=`.
   - **Non-2xx is a hard failure before parsing.** A GitHub 404 body is the 
text `404: Not Found`; JSON would reject it but CSV would index it as a 
document.
   - **No credentials and no caller-supplied headers, ever.** The server cannot 
become a credential relay.
   - **Redirects** followed manually, at most 5 hops, never `https` to `http`.
   - **Timeouts:** connect 10s, response headers 60s, plus a 30s read-idle 
watchdog and an optional byte cap (default unlimited). The watchdog exists 
because an endpoint that never closes the stream would otherwise keep the 
server committing batches indefinitely under the remote party's control, which 
has no filesystem analogue.
   - **Charset** from `Content-Type`, else UTF-8. No gzip. No `file:` or `ftp:`.
   
   ## Decisions and why
   
   1. **Any http(s) URL the server can reach is allowed; no operator 
allow-list.** Mirrors the posture `index-file` documents ("OS permissions and 
container mounts define the boundary"). The URL analogue is "the network the 
server process can reach". An allow-list would make the common case (a public 
raw-GitHub or S3 URL) need operator setup. This means the tool is a deliberate, 
documented SSRF surface in HTTP mode, and the threat model is updated to say so 
(below).
   2. **Link-local and cloud-metadata addresses (`169.254.0.0/16`, `fe80::/10`, 
`fd00:ec2::254`) are refused.** Not an allow-list, needs no configuration, 
removes the single worst outcome (a cloud instance's IAM credentials) without 
touching any legitimate use. Loopback and RFC1918 are deliberately not refused. 
*Open for maintainer input.*
   3. **`index-file` stays, STDIO-only.** This knowingly keeps the bean shape 
#194 was closed for. Reason: the flagship deployment is Claude Desktop + STDIO 
with the file on the same machine, and without `index-file` that deployment's 
most natural gesture takes the worst path while the file sits readable by the 
server process. With `index-url` present, `index-file` becomes a zero-network 
shortcut rather than a capability HTTP lacks. The out-of-band upload endpoint 
#194 mentioned is deferred, not rejected. *Open for maintainer input; the 
fallback is to ship `index-url` alone.*
   4. **One tool with optional `format`, not four per-format tools.** #197 kept 
per-format inline tools because each signature is optimised for its format's 
token shape. `index-url` carries no payload, so there is no shape to optimise.
   5. **No schema-from-URL.** Schema payloads are a few KB and the model should 
read and reconcile them against `get-schema` anyway.
   6. **`text/html` is an error, not converted.** "Index this web page" is a 
different feature.
   
   ## What HTTP mode still cannot do
   
   A file selected in Claude Desktop while connected to an HTTP-mode server has 
no bulk path. `index-file` is not registered there, and `index-url` resolves 
the URL from the **server's** network position, so 
`http://localhost:8000/file.json` served on the caller's machine hits the 
server's own loopback instead. Under decision 1 that does not fail cleanly: 
whatever the server is running on that port answers. This is structural until 
SEP-2631 lands; the tutorial will say "use the STDIO transport for local 
files", and the tool description says the URL is fetched from the server's side.
   
   ## Threat model impact
   
   `THREAT_MODEL.md` §8 property 5 currently says the AI client "cannot repoint 
the server or inject a target URL", with violation at critical severity, and 
§12 lists "backend-target from a tool argument" as a model-changing condition. 
This tool exercises that condition on purpose. The PR narrows §8.5 to the 
*Solr* backend and its credentials, adds the outbound fetch as a §9 disclaimed 
property, records the decision in §12 with the date, adds the `index-url` 
non-finding to §11a and the "internal services reachable from the HTTP 
deployment" misuse to §11, qualifies the §13 `VALID` row, adds the two 
body-limit knobs to §5a, and updates `docs/security/stdio.md` and `http.md`, 
which §8.5 cites. Without the §8.5 edit the next scanner files this as a 
critical finding regardless of the rest.
   
   ## Tests
   
   - `UrlIndexingIntegrationTest`: a JDK `com.sun.net.httpserver.HttpServer` on 
an ephemeral port (no new dependencies, runs under `nativeTest`) serving all 
four formats, a `.csv?token=x` URL, a media-type-only URL, `text/plain` and 
`text/html` bodies, a 404, a redirect, a redirect loop, a generated multi-MB 
CSV to prove the body streams, and a stalling handler to prove the watchdog; 
real Solr via Testcontainers.
   - `McpClientIntegrationTest` (HTTP) becomes the transport-parity guard: 
`index-url` present, `index-file` absent, one round trip. 
`McpClientStdioIntegrationTest`: both present, one round trip.
   - Unit tests for the target policy, the idle watchdog, and the error mapping.
   - Zero skipped tests in `./gradlew build` and `./gradlew nativeTest 
-Pnative`. A native spike (`HttpClient` over https in the native binary) runs 
before any test is written.
   
   ## Delivery
   
   Two PRs, in order: first the unrelated `required`-explicit refactor already 
on the branch, then the streaming spine + `index-file` + `index-url` + all doc 
and threat-model edits together (the spine is ~1,100 lines and is not 
reviewable without a consumer). Independent of #196, #202, #203, #205 and #207.
   
   ## Related
   
   - #194 (closed): STDIO-only `index-file`, the objection this design answers.
   - #197 (closed): the payload-cost measurement and the per-format-tools 
decision.
   - #88 (closed): earlier file-upload attempt.
   - SEP-2631: the MCP file-transfer draft that would close the HTTP local-file 
gap.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to