Skip to content

Commit a40098e

Browse files
aksOpsclaude
andcommitted
Add readFile line range support, startup optimization, and cleanup unused imports
- McpTools.readFile: add optional startLine/endLine params for reading specific line ranges from source files (1-based, inclusive) - GraphController /api/file: add startLine/endLine query params matching MCP tool - application.yml: enable lazy-initialization for indexing profile to speed up CLI commands that don't need full Spring context (e.g. version, plugins) - QueryService: remove unused imports (Arrays, HashMap, EdgeKind, NodeKind) - Add tests for line range reading in both McpToolsTest and GraphControllerTest Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3d0c187 commit a40098e

6 files changed

Lines changed: 119 additions & 11 deletions

File tree

src/main/java/io/github/randomcodespace/iq/api/GraphController.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,10 @@ public List<Map<String, Object>> searchGraph(
201201
}
202202

203203
@GetMapping("/file")
204-
public ResponseEntity<String> readFile(@RequestParam String path) {
204+
public ResponseEntity<String> readFile(
205+
@RequestParam String path,
206+
@RequestParam(required = false) Integer startLine,
207+
@RequestParam(required = false) Integer endLine) {
205208
Path codebasePath = Path.of(config.getRootPath()).toAbsolutePath().normalize();
206209
Path resolved = codebasePath.resolve(path).normalize();
207210
if (!resolved.startsWith(codebasePath)) {
@@ -214,6 +217,19 @@ public ResponseEntity<String> readFile(@RequestParam String path) {
214217
}
215218
try {
216219
String content = Files.readString(resolved, StandardCharsets.UTF_8);
220+
if (startLine != null || endLine != null) {
221+
String[] lines = content.split("\n", -1);
222+
int start = (startLine != null ? startLine : 1);
223+
int end = (endLine != null ? endLine : lines.length);
224+
start = Math.max(1, Math.min(start, lines.length));
225+
end = Math.max(start, Math.min(end, lines.length));
226+
StringBuilder sb = new StringBuilder();
227+
for (int i = start - 1; i < end; i++) {
228+
if (i > start - 1) sb.append('\n');
229+
sb.append(lines[i]);
230+
}
231+
content = sb.toString();
232+
}
217233
return ResponseEntity.ok()
218234
.contentType(MediaType.TEXT_PLAIN)
219235
.body(content);

src/main/java/io/github/randomcodespace/iq/mcp/McpTools.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,17 +267,34 @@ public String searchGraph(
267267
return toJson(queryService.searchGraph(query, limit != null ? limit : 20));
268268
}
269269

270-
@Tool(name = "read_file", description = "Read a source file's content for deep analysis. Path is relative to the codebase root.")
270+
@Tool(name = "read_file", description = "Read a source file from the codebase, optionally a specific line range")
271271
public String readFile(
272-
@ToolParam(description = "File path relative to codebase root") String filePath) {
272+
@ToolParam(description = "File path relative to codebase root") String filePath,
273+
@ToolParam(description = "Start line (1-based, optional)", required = false) Integer startLine,
274+
@ToolParam(description = "End line (1-based, inclusive, optional)", required = false) Integer endLine) {
273275
try {
274276
Path root = Path.of(config.getRootPath()).toAbsolutePath().normalize();
275277
Path resolved = root.resolve(filePath).normalize();
276278
// Path traversal protection
277279
if (!resolved.startsWith(root)) {
278280
return "Error: Path traversal detected";
279281
}
280-
return java.nio.file.Files.readString(resolved, java.nio.charset.StandardCharsets.UTF_8);
282+
String content = java.nio.file.Files.readString(resolved, java.nio.charset.StandardCharsets.UTF_8);
283+
if (startLine != null || endLine != null) {
284+
String[] lines = content.split("\n", -1);
285+
int start = (startLine != null ? startLine : 1);
286+
int end = (endLine != null ? endLine : lines.length);
287+
// Clamp bounds
288+
start = Math.max(1, Math.min(start, lines.length));
289+
end = Math.max(start, Math.min(end, lines.length));
290+
StringBuilder sb = new StringBuilder();
291+
for (int i = start - 1; i < end; i++) {
292+
if (i > start - 1) sb.append('\n');
293+
sb.append(lines[i]);
294+
}
295+
return sb.toString();
296+
}
297+
return content;
281298
} catch (Exception e) {
282299
return "Error: " + e.getMessage();
283300
}

src/main/java/io/github/randomcodespace/iq/query/QueryService.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,10 @@
44
import io.github.randomcodespace.iq.graph.GraphStore;
55
import io.github.randomcodespace.iq.model.CodeEdge;
66
import io.github.randomcodespace.iq.model.CodeNode;
7-
import io.github.randomcodespace.iq.model.EdgeKind;
8-
import io.github.randomcodespace.iq.model.NodeKind;
97
import org.springframework.cache.annotation.Cacheable;
108
import org.springframework.stereotype.Service;
119

1210
import java.util.ArrayList;
13-
import java.util.Arrays;
14-
import java.util.HashMap;
1511
import java.util.LinkedHashMap;
1612
import java.util.List;
1713
import java.util.Map;

src/main/resources/application.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ codeiq:
2525
path: ".osscodeiq/graph.db"
2626
max-depth: 10
2727
max-radius: 10
28+
batch-size: 500
2829

2930
spring.ai.mcp.server:
3031
name: code-iq
@@ -36,10 +37,16 @@ spring:
3637
config:
3738
activate:
3839
on-profile: indexing
40+
main:
41+
lazy-initialization: true
3942
autoconfigure:
4043
exclude:
4144
- org.springframework.ai.mcp.server.autoconfigure.McpServerAutoConfiguration
4245

46+
codeiq:
47+
neo4j:
48+
enabled: false
49+
4350
---
4451
spring:
4552
config:

src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,34 @@ void readFileShouldBlockPathTraversal(@TempDir Path tempDir) throws Exception {
456456
.andExpect(content().string("Path traversal blocked"));
457457
}
458458

459+
@Test
460+
void readFileShouldReturnLineRange(@TempDir Path tempDir) throws Exception {
461+
Files.writeString(tempDir.resolve("multi.txt"), "line1\nline2\nline3\nline4\nline5",
462+
StandardCharsets.UTF_8);
463+
config.setRootPath(tempDir.toAbsolutePath().toString());
464+
var controller = new GraphController(queryService, analyzer, config, statsService);
465+
var fileMvc = MockMvcBuilders.standaloneSetup(controller).build();
466+
467+
fileMvc.perform(get("/api/file")
468+
.param("path", "multi.txt")
469+
.param("startLine", "2")
470+
.param("endLine", "4"))
471+
.andExpect(status().isOk())
472+
.andExpect(content().string("line2\nline3\nline4"));
473+
}
474+
475+
@Test
476+
void readFileShouldReturnFullContentWithoutLineParams(@TempDir Path tempDir) throws Exception {
477+
Files.writeString(tempDir.resolve("full.txt"), "aaa\nbbb\nccc", StandardCharsets.UTF_8);
478+
config.setRootPath(tempDir.toAbsolutePath().toString());
479+
var controller = new GraphController(queryService, analyzer, config, statsService);
480+
var fileMvc = MockMvcBuilders.standaloneSetup(controller).build();
481+
482+
fileMvc.perform(get("/api/file").param("path", "full.txt"))
483+
.andExpect(status().isOk())
484+
.andExpect(content().string("aaa\nbbb\nccc"));
485+
}
486+
459487
// --- /api/analyze ---
460488

461489
@Test

src/test/java/io/github/randomcodespace/iq/mcp/McpToolsTest.java

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ void readFileShouldReadContent(@TempDir Path tempDir) throws IOException {
485485
Path file = tempDir.resolve("test.txt");
486486
Files.writeString(file, "Hello, World!");
487487

488-
String result = mcpTools.readFile("test.txt");
488+
String result = mcpTools.readFile("test.txt", null, null);
489489

490490
assertEquals("Hello, World!", result);
491491
}
@@ -494,7 +494,7 @@ void readFileShouldReadContent(@TempDir Path tempDir) throws IOException {
494494
void readFileShouldRejectPathTraversal(@TempDir Path tempDir) {
495495
config.setRootPath(tempDir.toString());
496496

497-
String result = mcpTools.readFile("../../etc/passwd");
497+
String result = mcpTools.readFile("../../etc/passwd", null, null);
498498

499499
assertEquals("Error: Path traversal detected", result);
500500
}
@@ -503,8 +503,52 @@ void readFileShouldRejectPathTraversal(@TempDir Path tempDir) {
503503
void readFileShouldHandleMissingFile(@TempDir Path tempDir) {
504504
config.setRootPath(tempDir.toString());
505505

506-
String result = mcpTools.readFile("nonexistent.txt");
506+
String result = mcpTools.readFile("nonexistent.txt", null, null);
507507

508508
assertTrue(result.startsWith("Error:"));
509509
}
510+
511+
@Test
512+
void readFileShouldReturnLineRange(@TempDir Path tempDir) throws IOException {
513+
config.setRootPath(tempDir.toString());
514+
Path file = tempDir.resolve("lines.txt");
515+
Files.writeString(file, "line1\nline2\nline3\nline4\nline5");
516+
517+
String result = mcpTools.readFile("lines.txt", 2, 4);
518+
519+
assertEquals("line2\nline3\nline4", result);
520+
}
521+
522+
@Test
523+
void readFileShouldReturnFromStartLineToEnd(@TempDir Path tempDir) throws IOException {
524+
config.setRootPath(tempDir.toString());
525+
Path file = tempDir.resolve("lines.txt");
526+
Files.writeString(file, "line1\nline2\nline3\nline4\nline5");
527+
528+
String result = mcpTools.readFile("lines.txt", 3, null);
529+
530+
assertEquals("line3\nline4\nline5", result);
531+
}
532+
533+
@Test
534+
void readFileShouldReturnFromStartToEndLine(@TempDir Path tempDir) throws IOException {
535+
config.setRootPath(tempDir.toString());
536+
Path file = tempDir.resolve("lines.txt");
537+
Files.writeString(file, "line1\nline2\nline3\nline4\nline5");
538+
539+
String result = mcpTools.readFile("lines.txt", null, 2);
540+
541+
assertEquals("line1\nline2", result);
542+
}
543+
544+
@Test
545+
void readFileShouldClampOutOfBoundsLineRange(@TempDir Path tempDir) throws IOException {
546+
config.setRootPath(tempDir.toString());
547+
Path file = tempDir.resolve("lines.txt");
548+
Files.writeString(file, "line1\nline2\nline3");
549+
550+
String result = mcpTools.readFile("lines.txt", 2, 100);
551+
552+
assertEquals("line2\nline3", result);
553+
}
510554
}

0 commit comments

Comments
 (0)