@takara-ai/miru-code 0.4.6 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp/server.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import type { ContentType } from "../types.ts";
4
- import { formatResults, resolveChunk } from "../utils.ts";
4
+ import { formatResults, localRepoRoot, resolveChunk } from "../utils.ts";
5
5
  import { getIndexForRepo, type IndexCache, toolText } from "./index-cache.ts";
6
6
 
7
7
  const REPO_DESCRIPTION =
@@ -10,11 +10,11 @@ const REPO_DESCRIPTION =
10
10
  "The index is built on the first tool call and cached for the session.";
11
11
 
12
12
  const SERVER_INSTRUCTIONS =
13
- "Instant code search for any local or remote git repository. " +
14
- "Call `search` to find relevant code; call `find_related` on a result to discover similar code elsewhere. " +
15
- "Always pass the project root as `repo` on the first call (local path or https:// git URL). " +
16
- "Never guess or infer URLs. " +
17
- "Prefer these tools over Grep, Glob, or Read for any question about how code works.";
13
+ "DO NOT use Grep, Glob, SemanticSearch, or Read to explore code when this server is available. " +
14
+ "Call `search` first for any question about how code works; call `find_related` on a hit to explore neighbors. " +
15
+ "Always pass the project root as `repo` (local path or https:// git URL). Never guess URLs. " +
16
+ "Local repos return `absolute_path` on each hit — use it for Read/edit. " +
17
+ "Native search is only for exact literal confirmation or files outside the indexed repo.";
18
18
 
19
19
  export function createMcpServer(cache: IndexCache): McpServer {
20
20
  const server = new McpServer(
@@ -31,8 +31,8 @@ export function createMcpServer(cache: IndexCache): McpServer {
31
31
  "search",
32
32
  {
33
33
  description:
34
- "Search a codebase with a natural-language or code query. " +
35
- "Indexes `repo` on the first call; later calls reuse the session cache.",
34
+ "DEFAULT code exploration tool. DO NOT use Grep, Glob, SemanticSearch, or Read instead. " +
35
+ "Search with natural language or a code query. Indexes `repo` on first call; later calls reuse the session cache.",
36
36
  inputSchema: {
37
37
  query: z.string().describe("Natural language or code query."),
38
38
  repo: z.string().describe(REPO_DESCRIPTION),
@@ -46,7 +46,8 @@ export function createMcpServer(cache: IndexCache): McpServer {
46
46
  if (results.length === 0) {
47
47
  return toolText(JSON.stringify({ error: "No results found." }));
48
48
  }
49
- return toolText(JSON.stringify(formatResults(query, results)));
49
+ const repoRoot = localRepoRoot(repo);
50
+ return toolText(JSON.stringify(formatResults(query, results, { repoRoot })));
50
51
  } catch (err) {
51
52
  return toolText(err instanceof Error ? err.message : String(err));
52
53
  }
@@ -57,13 +58,13 @@ export function createMcpServer(cache: IndexCache): McpServer {
57
58
  "find_related",
58
59
  {
59
60
  description:
60
- "Find code chunks semantically similar to a specific location in a file. " +
61
- "Use after `search` to explore related implementations or callers.",
61
+ "Find code similar to a file:line from a prior `search` hit. " +
62
+ "Use instead of Grep chains or reading whole files to trace related code.",
62
63
  inputSchema: {
63
64
  file_path: z
64
65
  .string()
65
66
  .describe(
66
- "Path to the file as stored in the index (use file_path from a search result).",
67
+ "Path from a search hit (`file_path` or `absolute_path` for local repos).",
67
68
  ),
68
69
  line: z.number().int().describe("Line number (1-indexed)."),
69
70
  repo: z.string().describe(REPO_DESCRIPTION),
@@ -73,7 +74,8 @@ export function createMcpServer(cache: IndexCache): McpServer {
73
74
  async ({ file_path: filePath, line, repo, top_k: topK }) => {
74
75
  try {
75
76
  const index = await getIndexForRepo(repo, cache);
76
- const chunk = resolveChunk(index.chunks, filePath, line);
77
+ const repoRoot = localRepoRoot(repo);
78
+ const chunk = resolveChunk(index.chunks, filePath, line, repoRoot);
77
79
  if (!chunk) {
78
80
  return toolText(
79
81
  `No chunk found at ${filePath}:${line}. Make sure the file is indexed and the line number is within a known chunk.`,
@@ -86,7 +88,9 @@ export function createMcpServer(cache: IndexCache): McpServer {
86
88
  );
87
89
  }
88
90
  return toolText(
89
- JSON.stringify(formatResults(`Chunks related to ${filePath}:${line}`, results)),
91
+ JSON.stringify(
92
+ formatResults(`Chunks related to ${filePath}:${line}`, results, { repoRoot }),
93
+ ),
90
94
  );
91
95
  } catch (err) {
92
96
  return toolText(err instanceof Error ? err.message : String(err));
package/src/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { resolve } from "node:path";
1
+ import { join, relative, resolve, sep } from "node:path";
2
2
  import type { Chunk, ContentType, SearchResult } from "./types.ts";
3
- import { searchResultToDict } from "./types.ts";
3
+ import { chunkToDict } from "./types.ts";
4
4
 
5
5
  const GIT_URL_SCHEMES = [
6
6
  "https://",
@@ -23,10 +23,38 @@ export function isAllowedRepoSource(repo: string): boolean {
23
23
  return repo.startsWith("https://") || repo.startsWith("http://");
24
24
  }
25
25
 
26
- export function resolveChunk(chunks: Chunk[], filePath: string, line: number): Chunk | null {
26
+ /** Resolved local repo root for MCP output; null for remote git URLs. */
27
+ export function localRepoRoot(repo: string): string | null {
28
+ return isGitUrl(repo) ? null : resolve(repo);
29
+ }
30
+
31
+ /** Map an absolute or repo-relative path to the index-relative form. */
32
+ export function toIndexedFilePath(filePath: string, repoRoot?: string | null): string {
33
+ if (!repoRoot) {
34
+ return filePath;
35
+ }
36
+ const root = resolve(repoRoot);
37
+ const candidate = resolve(root, filePath);
38
+ if (candidate === root) {
39
+ return "";
40
+ }
41
+ const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
42
+ if (candidate.startsWith(prefix)) {
43
+ return relative(root, candidate).replace(/\\/g, "/");
44
+ }
45
+ return filePath;
46
+ }
47
+
48
+ export function resolveChunk(
49
+ chunks: Chunk[],
50
+ filePath: string,
51
+ line: number,
52
+ repoRoot?: string | null,
53
+ ): Chunk | null {
54
+ const indexedPath = toIndexedFilePath(filePath, repoRoot);
27
55
  let fallback: Chunk | null = null;
28
56
  for (const chunk of chunks) {
29
- if (chunk.file_path === filePath && chunk.start_line <= line && line <= chunk.end_line) {
57
+ if (chunk.file_path === indexedPath && chunk.start_line <= line && line <= chunk.end_line) {
30
58
  if (line < chunk.end_line) {
31
59
  return chunk;
32
60
  }
@@ -38,13 +66,31 @@ export function resolveChunk(chunks: Chunk[], filePath: string, line: number): C
38
66
  return fallback;
39
67
  }
40
68
 
69
+ function chunkToResponseDict(chunk: Chunk, repoRoot?: string | null): Record<string, unknown> {
70
+ const dict = chunkToDict(chunk);
71
+ if (!repoRoot) {
72
+ return dict;
73
+ }
74
+ const absolutePath = join(resolve(repoRoot), chunk.file_path);
75
+ return {
76
+ ...dict,
77
+ absolute_path: absolutePath,
78
+ location: `${absolutePath}:${chunk.start_line}-${chunk.end_line}`,
79
+ };
80
+ }
81
+
41
82
  export function formatResults(
42
83
  query: string,
43
84
  results: SearchResult[],
85
+ options?: { repoRoot?: string | null },
44
86
  ): { query: string; results: Record<string, unknown>[] } {
87
+ const repoRoot = options?.repoRoot ?? null;
45
88
  return {
46
89
  query,
47
- results: results.map(searchResultToDict),
90
+ results: results.map((result) => ({
91
+ chunk: chunkToResponseDict(result.chunk, repoRoot),
92
+ score: result.score,
93
+ })),
48
94
  };
49
95
  }
50
96