Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,28 +115,70 @@ mimir analyze --hint <file> <path> # Hint for faster patch planning

## MCP Tools

AI agents get access to 7 tools via MCP:
AI agents get access to 12 tools via MCP:

| Tool | Description | Example |
|---|---|---|
| `query` | Hybrid search (BM25 + vector) | "Find all auth-related processes" |
| `context` | 360-degree symbol view | "Show handleRequest definition and callers" |
| `find_referencing` | Who directly calls/imports/extends a symbol (1-hop) | "What calls UserService.GetUser?" |
| `symbol_coordinates` | Exact file path + line range for a symbol | "Where is ProcessOrder defined?" |
| `get_symbols_overview` | All top-level symbols in a file, sorted by line | "What's exported from store.go?" |
| `impact` | Blast radius analysis | "What breaks if I change UserService?" |
| `detect_changes` | Analyze uncommitted git changes | "What processes did my commit affect?" |
| `rename` | Plan coordinated multi-file rename | "Rename AuthController to SessionController" |
| `cypher` | Raw graph queries | "Find unused exported functions" |
| `list_repos` | List indexed repositories | — |
| `list_repos` | List all registered repositories | — |
| `query_repo` | Run a read-only tool against a different repo | "Query symbols in repo B from repo A" |

### Recommended Workflow

1. **Discovery**: Use `query()` for semantic search
2. **Deep Dive**: Use `context()` to understand a symbol
3. **Before Editing**: Always run `impact()` first
3. **Find callers**: Use `find_referencing()` for a lightweight 1-hop caller list
4. **Understand a file**: Use `get_symbols_overview()` to see what's defined in a file
5. **Before Editing**: Run `symbol_coordinates()` to get the exact location, then `impact()` for blast radius
6. **Cross-repo**: Use `list_repos()` to discover repos, then `query_repo()` to query them

For detailed usage, see [docs/guide.md](docs/guide.md).

---

## Testing MCP Tools Locally

Use [MCP Inspector](https://github.com/modelcontextprotocol/inspector) — the official browser-based UI for exploring and calling MCP tools interactively:

```bash
# Build first
make build

# Index the current repo
./bin/mimir analyze .

# Launch MCP Inspector (requires Node.js)
npx @modelcontextprotocol/inspector ./bin/mimir mcp
```

Then open **http://localhost:5173** in your browser. You can browse all 12 tools, fill in arguments via a form, and see the JSON responses in real time.

Alternatively, test via raw stdin:

```bash
# List all tools
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | ./bin/mimir mcp

# Call a tool
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_repos","arguments":{}}}' | ./bin/mimir mcp

# Query a symbol in the current repo
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"query","arguments":{"q":"handleRequest"}}}' | ./bin/mimir mcp

# Cross-repo query: search for a symbol in a different repo
echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"query_repo","arguments":{"tool_name":"query","arguments":{"query":"auth"},"target_repo":"other-project","current_repo":"git-mimir"}}}' | ./bin/mimir mcp
```

---

## Auto-Analyze Features

Running `mimir analyze` automatically sets up:
Expand Down
74 changes: 52 additions & 22 deletions cmd/mimir/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"strings"
"time"

"github.com/schollz/progressbar/v3"

"github.com/spf13/cobra"
"github.com/thuongh2/git-mimir/internal/cluster"
"github.com/thuongh2/git-mimir/internal/daemon"
Expand All @@ -26,17 +28,17 @@ import (
)

var (
analyzeForce bool
analyzeSkipEmbeds bool
analyzeResolution float64
analyzeName string
analyzeIncremental bool
analyzeHint string
analyzeRepo string
analyzeSkipHooks bool
analyzeSkipSkills bool
analyzeSkipDaemon bool
analyzeQuiet bool
analyzeForce bool
analyzeSkipEmbeds bool
analyzeResolution float64
analyzeName string
analyzeIncremental bool
analyzeHint string
analyzeRepo string
analyzeSkipHooks bool
analyzeSkipSkills bool
analyzeSkipDaemon bool
analyzeQuiet bool
)

func init() {
Expand Down Expand Up @@ -342,29 +344,57 @@ func fullIndex(ctx context.Context, repoPath string, s *store.Store) ([]graph.Fi
concurrency := runtime.GOMAXPROCS(0)
start := time.Now()

fileCh := walker.WalkRepo(repoPath, concurrency)
// Walk synchronously so we know the total before parsing starts.
allFiles, err := walker.CollectFiles(repoPath)
if err != nil {
return nil, 0, fmt.Errorf("walk: %w", err)
}
// Pre-filter to supported extensions so total matches actual parser output.
files := allFiles[:0]
for _, f := range allFiles {
if parser.LangForExt(f.Ext) != "" {
files = append(files, f)
}
}
total := len(files)

// Count files while forwarding to parser pool
counted := make(chan walker.FileInfo, concurrency*16)
fileCount := 0
var bar *progressbar.ProgressBar
if !analyzeQuiet {
bar = progressbar.NewOptions(total,
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionSetDescription("Parsing"),
progressbar.OptionSetWidth(30),
progressbar.OptionShowCount(),
progressbar.OptionShowIts(),
progressbar.OptionSetItsString("files"),
progressbar.OptionOnCompletion(func() { fmt.Fprint(os.Stderr, "\n") }),
)
}

// Fan collected files into a channel for the parser pool.
fileCh := make(chan walker.FileInfo, concurrency*16)
go func() {
for f := range fileCh {
counted <- f
fileCount++
for _, f := range files {
fileCh <- f
}
close(counted)
close(fileCh)
}()

pool := parser.NewParserPool(concurrency)
symsCh := pool.Run(ctx, counted)
symsCh := pool.Run(ctx, fileCh)

var allSymbols []graph.FileSymbols
for fs := range symsCh {
allSymbols = append(allSymbols, fs)
if bar != nil {
_ = bar.Add(1)
}
}

fmt.Printf("Walked %s in %s\n", repoPath, time.Since(start).Round(time.Millisecond))
return allSymbols, fileCount, nil
if !analyzeQuiet {
fmt.Fprintf(os.Stderr, "Parsed %d files (%s)\n", len(allSymbols), time.Since(start).Round(time.Millisecond))
}
return allSymbols, len(allSymbols), nil
}

func isIncrementalCandidate(s *store.Store) bool {
Expand Down
14 changes: 9 additions & 5 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ Mimir provides a suite of features designed specifically for AI agents through t

| Tool | Description |
|---|---|
| `query` | Hybrid search (Keyword + Vector) that returns results grouped by logical processes. |
| `context` | Returns a "360-degree" view of a specific symbol, including its definition, callers, called functions, and related documentation. |
| `impact` | Performs blast-radius analysis to show what might break if a specific function or class is modified. |
| `query` | Hybrid search (BM25 + vector + centrality) that returns results grouped by logical processes. |
| `context` | Returns a "360-degree" view of a specific symbol: definition, outgoing calls, all incoming edges, and cluster membership. |
| `find_referencing` | Lightweight 1-hop lookup of all symbols that directly call, import, extend, or implement a given symbol. Accepts an optional `edge_types` filter (`CALLS`, `IMPORTS`, `EXTENDS`, `IMPLEMENTS`, `MEMBER_OF`) and `min_confidence` threshold. Faster than `context` when you only need the caller list. |
| `symbol_coordinates` | Returns the exact `file_path`, `start_line`, and `end_line` for every definition of a symbol. Always run this before editing a symbol body so the agent knows the precise location to modify. |
| `get_symbols_overview` | Returns all top-level symbols (functions, classes, interfaces, variables, constants) defined in a given file, sorted by line number. Excludes nested methods and members that belong to a class. Accepts `include_private` (default: `true`) to filter to exported symbols only. Use this to understand the structure of a file before editing. |
| `impact` | Performs blast-radius analysis (BFS up to configurable depth) to show what might break if a specific function or class is modified. Supports `direction` (upstream/downstream) and `min_confidence`. |
| `detect_changes` | Analyzes uncommitted git changes and identifies which high-level processes are affected. |
| `rename` | Plans a coordinated rename across the graph and the filesystem. |
| `cypher` | Allows advanced users to run graph queries using a subset of the Cypher query language. |
| `rename` | Plans a coordinated rename across the graph and the filesystem. Use `dry_run=true` first. |
| `cypher` | Allows advanced users to run graph queries using a subset of the Cypher query language (`MATCH` only). |
| `list_repos` | Lists all indexed repositories available for querying. |
| `query_repo` | Executes a read-only tool against a different indexed repository without switching context. Accepts `tool_name` (one of: `query`, `context`, `find_referencing`, `symbol_coordinates`, `get_symbols_overview`, `impact`), `arguments`, `target_repo`, and optional `current_repo` for correlation. The response includes a `meta` field with `queried_repo`, `current_repo`, and `tool_used`. |

## Key Features

Expand Down
14 changes: 14 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,23 @@ Before writing code, run `mimir serve` and open `http://localhost:7842`.
When chatting with Claude, you can prompt it to use Mimir effectively:

* **"Query the graph"**: Instead of letting Claude search files with `grep`, tell it: *"Use Mimir to find all processes related to 'user authentication'."* Mimir will return logical flows, not just lines of code.
* **"Find callers (lightweight)"**: Before touching a function, ask: *"Use `find_referencing` to show me every symbol that calls `ProcessOrder`."* This is faster than `context` when you only need the caller list — it does a single 1-hop edge lookup. You can also filter by edge type: *"find all symbols that **import** the logger package."*
* **"See what's in a file"**: Before editing a file, ask: *"Run `get_symbols_overview` on `internal/store/store.go`.*" This returns every top-level symbol sorted by line number — instantly orient yourself without reading the whole file. Use `include_private: false` to focus on the public API.
* **"Get exact location before editing"**: Before asking Claude to modify a function body, say: *"Run `symbol_coordinates` on `ProcessOrder` first."* Claude will know the exact file and lines to replace without reading the whole file.
* **"Check Impact"**: Before asking Claude to refactor, say: *"Run a Mimir impact analysis on the `AuthService` interface."* This prevents Claude from making breaking changes in distant parts of the repo.
* **"Trace the process"**: If a bug happens in a specific flow, say: *"Mimir, show me the full process trace starting from the `login` endpoint."*

### Recommended Tool Order for Editing a Symbol

```
1. query() → find candidate symbols
2. find_referencing() → who calls it? (decide scope of change)
3. get_symbols_overview() → understand the file's public surface
4. symbol_coordinates() → get file_path + line range
5. impact() → blast radius if it's a public API
6. make the edit → agent replaces lines start_line–end_line
```

## 5. Advanced: Raw Graph Queries (Cypher)
For power users, Mimir supports a subset of Cypher. You can ask Claude: *"Run a Cypher query to find all exported functions in the 'internal/store' package that have no incoming call edges."*

Expand Down
11 changes: 10 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ When using Claude Code to develop or query Mimir, use the following guidelines:

- **Always start with `query`**: If you don't know where to look, use `mcp__mimir__query` to find the relevant code paths.
- **Use `context` for deep dives**: Once you find a symbol, use `mcp__mimir__context` to see its full relationship tree.
- **Use `find_referencing` for lightweight caller lookup**: When you only need to know who calls or imports a symbol (not the full 360° view), `mcp__mimir__find_referencing` is faster. Supports `edge_types` filter — e.g., `["CALLS"]` or `["IMPORTS"]`.
- **Always run `symbol_coordinates` before editing**: Before replacing a function or class body, call `mcp__mimir__symbol_coordinates` to get the exact `file_path`, `start_line`, and `end_line`. This avoids reading entire files just to locate the target.
- **Check `impact` before refactoring**: If you are about to change a core interface, run `mcp__mimir__impact` to see the blast radius.
- **Analyze on save**: You can configure a git hook to run `mimir analyze` automatically on commit to keep the knowledge graph fresh.

## 3. Example Workflow
## 3. Example Workflow — Understanding a Symbol
1. **User**: "How does the resolver handle interfaces?"
2. **Claude**: Calls `mcp__mimir__query(query="interface resolution")`.
3. **Mimir**: Returns relevant symbols and the "Resolve" process.
4. **Claude**: Calls `mcp__mimir__context(name="Resolve")` to see the logic.
5. **Claude**: Explains the logic to the user using the high-fidelity graph data.

## 4. Example Workflow — Editing a Symbol Safely
1. **User**: "Refactor `ProcessOrder` to accept a context parameter."
2. **Claude**: Calls `mcp__mimir__find_referencing(name="ProcessOrder", edge_types=["CALLS"])` → sees 4 direct callers.
3. **Claude**: Calls `mcp__mimir__symbol_coordinates(name="ProcessOrder")` → gets `{file_path: "internal/order/service.go", start_line: 42, end_line: 67}`.
4. **Claude**: Calls `mcp__mimir__impact(target="ProcessOrder")` → confirms blast radius before changing the signature.
5. **Claude**: Edits lines 42–67 in `internal/order/service.go` and updates the 4 call sites.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,20 @@ require (
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-pointer v0.0.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/schollz/progressbar/v3 v3.19.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.41.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
Expand All @@ -72,11 +74,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
Expand Down Expand Up @@ -147,6 +153,8 @@ golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
Expand Down
34 changes: 34 additions & 0 deletions internal/store/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,40 @@ func (s *Store) QueryByFile(filePath string) ([]graph.Node, error) {
return nodes, err
}

// QueryTopLevelByFile returns all top-level symbols in a file — excludes nested
// methods and any symbol that is a target of a MEMBER_OF edge.
// Matches by exact path OR by suffix (handles relative vs absolute path mismatches).
func (s *Store) QueryTopLevelByFile(filePath string) ([]graph.Node, error) {
var nodes []graph.Node
err := s.Read(func(db *sql.DB) error {
rows, err := db.Query(`
SELECT uid, name, kind, file_path, start_line, end_line, exported, package_path, COALESCE(cluster_id,'')
FROM nodes
WHERE (file_path = ? OR file_path LIKE ('%/' || ?))
AND kind != 'Method'
AND NOT EXISTS (
SELECT 1 FROM edges WHERE from_uid = uid AND type = 'MEMBER_OF'
)
ORDER BY start_line`, filePath, filePath)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var n graph.Node
var exp int
if err := rows.Scan(&n.UID, &n.Name, &n.Kind, &n.FilePath,
&n.StartLine, &n.EndLine, &exp, &n.PackagePath, &n.ClusterID); err != nil {
return err
}
n.Exported = exp == 1
nodes = append(nodes, n)
}
return rows.Err()
})
return nodes, err
}

// QueryNodeByUID fetches a single node by UID.
func (s *Store) QueryNodeByUID(uid string) (*graph.Node, error) {
var n graph.Node
Expand Down
Loading
Loading