diff --git a/README.md b/README.md index 0a79d0a..a91ebb2 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs

Install clangd: Download prebuilt binaries from the official LLVM releases page or install via your system's package manager (e.g., apt install clangd, brew install clangd).

Configure your MCP client: This will be different but similar for each client. For Claude Desktop, add the following to ~/Library/Application\\ Support/Claude/claude_desktop_config.json

+

NOTE: clangd will not resolve symbols until the first file is opened. Use the `-open` argument to trigger indexing.

 {
@@ -140,6 +141,8 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs
         "/Users/you/dev/yourproject/",
         "--lsp",
         "/path/to/your/clangd_binary",
+        "--open",
+        "/Users/you/dev/yourproject/main.cpp",
         "--",
         "--compile-commands-dir=/path/to/yourproject/build_or_compile_commands_dir"
       ]
@@ -170,11 +173,14 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs
 ## Tools
 
 - `definition`: Retrieves the complete source code definition of any symbol (function, type, constant, etc.) from your codebase.
+- `content`: Retrieves the complete source code definition (function, type, constant, etc.) from your codebase at a specific location.
 - `references`: Locates all usages and references of a symbol throughout the codebase.
 - `diagnostics`: Provides diagnostic information for a specific file, including warnings and errors.
 - `hover`: Display documentation, type hints, or other hover information for a given location.
 - `rename_symbol`: Rename a symbol across a project.
 - `edit_file`: Allows making multiple text edits to a file based on line numbers. Provides a more reliable and context-economical way to edit files compared to search and replace based edit tools.
+- `callers`: Shows all locations that call a given symbol
+- `callees`: Shows all functions that a given symbol calls
 
 ## About
 
diff --git a/go.mod b/go.mod
index b875a3f..22f0c33 100644
--- a/go.mod
+++ b/go.mod
@@ -3,12 +3,13 @@ module github.com/isaacphi/mcp-language-server
 go 1.24.0
 
 require (
+	github.com/bmatcuk/doublestar/v4 v4.8.1
 	github.com/davecgh/go-spew v1.1.1
 	github.com/fsnotify/fsnotify v1.9.0
-	github.com/mark3labs/mcp-go v0.25.0
+	github.com/mark3labs/mcp-go v0.33.0
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
 	github.com/stretchr/testify v1.10.0
-	golang.org/x/text v0.25.0
+	golang.org/x/text v0.26.0
 )
 
 require (
@@ -21,11 +22,11 @@ require (
 	github.com/spf13/cast v1.7.1 // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
-	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/sync v0.14.0 // indirect
-	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/mod v0.25.0 // indirect
+	golang.org/x/sync v0.15.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
-	golang.org/x/tools v0.31.0 // indirect
+	golang.org/x/tools v0.33.0 // indirect
 	golang.org/x/vuln v1.1.4 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 9ae5405..148264b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
 github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
+github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -24,8 +26,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/mark3labs/mcp-go v0.25.0 h1:UUpcMT3L5hIhuDy7aifj4Bphw4Pfx1Rf8mzMXDe8RQw=
-github.com/mark3labs/mcp-go v0.25.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
+github.com/mark3labs/mcp-go v0.33.0 h1:naxhjnTIs/tyPZmWUZFuG0lDmdA6sUyYGGf3gsHvTCc=
+github.com/mark3labs/mcp-go v0.33.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -42,18 +44,18 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI
 github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
 golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
 golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
-golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
+golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
 golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
+golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
 golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
 golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/integrationtests/snapshots/clangd/definition/class.snap b/integrationtests/snapshots/clangd/definition/class.snap
index 8b1a97b..9b707ac 100644
--- a/integrationtests/snapshots/clangd/definition/class.snap
+++ b/integrationtests/snapshots/clangd/definition/class.snap
@@ -2,6 +2,7 @@
 
 Symbol: TestClass
 /TEST_OUTPUT/workspace/clangd/src/consumer.cpp
+Kind: Class
 Range: L7:C1 - L15:C2
 
  7|class TestClass {
diff --git a/integrationtests/snapshots/clangd/definition/constant.snap b/integrationtests/snapshots/clangd/definition/constant.snap
index 90253d9..67cba96 100644
--- a/integrationtests/snapshots/clangd/definition/constant.snap
+++ b/integrationtests/snapshots/clangd/definition/constant.snap
@@ -2,6 +2,7 @@
 
 Symbol: TEST_CONSTANT
 /TEST_OUTPUT/workspace/clangd/src/helper.cpp
+Kind: Variable
 Range: L4:C1 - L4:C29
 
 4|const int TEST_CONSTANT = 42;
diff --git a/integrationtests/snapshots/clangd/definition/foobar.snap b/integrationtests/snapshots/clangd/definition/foobar.snap
index 0ad3994..0bbdead 100644
--- a/integrationtests/snapshots/clangd/definition/foobar.snap
+++ b/integrationtests/snapshots/clangd/definition/foobar.snap
@@ -2,6 +2,7 @@
 
 Symbol: foo_bar
 /TEST_OUTPUT/workspace/src/main.cpp
+Kind: Function
 Range: L5:C1 - L8:C2
 
 5|void foo_bar() {
diff --git a/integrationtests/snapshots/clangd/definition/helperFunction.snap b/integrationtests/snapshots/clangd/definition/helperFunction.snap
index 4b9d978..2ea8f5e 100644
--- a/integrationtests/snapshots/clangd/definition/helperFunction.snap
+++ b/integrationtests/snapshots/clangd/definition/helperFunction.snap
@@ -2,6 +2,7 @@
 
 Symbol: helperFunction
 /TEST_OUTPUT/workspace/clangd/src/helper.cpp
+Kind: Function
 Range: L7:C1 - L7:C71
 
 7|void helperFunction() { std::cout << "Helper function" << std::endl; }
diff --git a/integrationtests/snapshots/clangd/definition/method.snap b/integrationtests/snapshots/clangd/definition/method.snap
index 1ec50b2..b02a917 100644
--- a/integrationtests/snapshots/clangd/definition/method.snap
+++ b/integrationtests/snapshots/clangd/definition/method.snap
@@ -2,15 +2,9 @@
 
 Symbol: method
 /TEST_OUTPUT/workspace/clangd/src/consumer.cpp
-Range: L7:C1 - L15:C2
+Kind: Method
+Container Name: TestClass
+Range: L14:C1 - L14:C47
 
- 7|class TestClass {
- 8| public:
- 9|  /**
-10|   * @brief A method that takes an integer parameter.
-11|   *
-12|   * @param param The integer parameter to be processed.
-13|   */
 14|  void method(int param) { helperFunction(); }
-15|};
 
diff --git a/integrationtests/snapshots/clangd/definition/nsFunction.snap b/integrationtests/snapshots/clangd/definition/nsFunction.snap
new file mode 100644
index 0000000..5076b99
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/nsFunction.snap
@@ -0,0 +1,12 @@
+---
+
+Symbol: nsFunction2
+/TEST_OUTPUT/workspace/clangd/src/namespace.cpp
+Kind: Function
+Container Name: ns
+Range: L11:C1 - L13:C2
+
+11|void nsFunction2() {
+12|    // empty   
+13|}
+
diff --git a/integrationtests/snapshots/clangd/definition/struct.snap b/integrationtests/snapshots/clangd/definition/struct.snap
index ff5711d..12f1eca 100644
--- a/integrationtests/snapshots/clangd/definition/struct.snap
+++ b/integrationtests/snapshots/clangd/definition/struct.snap
@@ -2,6 +2,7 @@
 
 Symbol: TestStruct
 /TEST_OUTPUT/workspace/clangd/src/types.cpp
+Kind: Class
 Range: L6:C1 - L8:C2
 
 6|struct TestStruct {
diff --git a/integrationtests/snapshots/clangd/definition/type.snap b/integrationtests/snapshots/clangd/definition/type.snap
index c9bb0fc..188d79a 100644
--- a/integrationtests/snapshots/clangd/definition/type.snap
+++ b/integrationtests/snapshots/clangd/definition/type.snap
@@ -2,6 +2,7 @@
 
 Symbol: TestType
 /TEST_OUTPUT/workspace/clangd/src/types.cpp
+Kind: Class
 Range: L10:C1 - L10:C21
 
 10|using TestType = int;
diff --git a/integrationtests/snapshots/clangd/definition/variable.snap b/integrationtests/snapshots/clangd/definition/variable.snap
index c9d1442..33307d5 100644
--- a/integrationtests/snapshots/clangd/definition/variable.snap
+++ b/integrationtests/snapshots/clangd/definition/variable.snap
@@ -2,6 +2,7 @@
 
 Symbol: TEST_VARIABLE
 /TEST_OUTPUT/workspace/clangd/src/helper.cpp
+Kind: Variable
 Range: L5:C1 - L5:C24
 
 5|int TEST_VARIABLE = 100;  // A test variable used for integration testing purposes.
diff --git a/integrationtests/snapshots/go/call_hierarchy/incoming-other-file.snap b/integrationtests/snapshots/go/call_hierarchy/incoming-other-file.snap
new file mode 100644
index 0000000..c282e40
--- /dev/null
+++ b/integrationtests/snapshots/go/call_hierarchy/incoming-other-file.snap
@@ -0,0 +1,14 @@
+
+---
+Name: HelperFunction
+Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • helper.go
+/TEST_OUTPUT/workspace/helper.go
+Range: L4:C6 - L4:C20
+- Called By: AnotherConsumer
+  Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • another_consumer.go
+/TEST_OUTPUT/workspace/another_consumer.go
+  Range: L6:C6 - L6:C21
+- Called By: ConsumerFunction
+  Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • consumer.go
+/TEST_OUTPUT/workspace/consumer.go
+  Range: L6:C6 - L6:C22
diff --git a/integrationtests/snapshots/go/call_hierarchy/incoming-same-file.snap b/integrationtests/snapshots/go/call_hierarchy/incoming-same-file.snap
new file mode 100644
index 0000000..d49c425
--- /dev/null
+++ b/integrationtests/snapshots/go/call_hierarchy/incoming-same-file.snap
@@ -0,0 +1,10 @@
+
+---
+Name: FooBar
+Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • main.go
+/TEST_OUTPUT/workspace/main.go
+Range: L6:C6 - L6:C12
+- Called By: main
+  Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • main.go
+/TEST_OUTPUT/workspace/main.go
+  Range: L12:C6 - L12:C10
diff --git a/integrationtests/snapshots/go/call_hierarchy/outgoing-other-file.snap b/integrationtests/snapshots/go/call_hierarchy/outgoing-other-file.snap
new file mode 100644
index 0000000..65b93b0
--- /dev/null
+++ b/integrationtests/snapshots/go/call_hierarchy/outgoing-other-file.snap
@@ -0,0 +1,26 @@
+
+---
+Name: ConsumerFunction
+Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • consumer.go
+/TEST_OUTPUT/workspace/consumer.go
+Range: L6:C6 - L6:C22
+- Calls: GetName
+  Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • types.go
+/TEST_OUTPUT/workspace/types.go
+  Range: L21:C2 - L21:C9
+- Calls: HelperFunction
+  Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • helper.go
+/TEST_OUTPUT/workspace/helper.go
+  Range: L4:C6 - L4:C20
+- Calls: Method
+  Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • types.go
+/TEST_OUTPUT/workspace/types.go
+  Range: L14:C24 - L14:C30
+- Calls: Println
+  Detail: fmt • print.go
+/GOROOT/src/fmt/print.go
+  Range: L313:C6 - L313:C13
+- Calls: Process
+  Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • types.go
+/TEST_OUTPUT/workspace/types.go
+  Range: L31:C24 - L31:C31
diff --git a/integrationtests/snapshots/go/call_hierarchy/outgoing-same-file.snap b/integrationtests/snapshots/go/call_hierarchy/outgoing-same-file.snap
new file mode 100644
index 0000000..54be4ba
--- /dev/null
+++ b/integrationtests/snapshots/go/call_hierarchy/outgoing-same-file.snap
@@ -0,0 +1,14 @@
+
+---
+Name: main
+Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • main.go
+/TEST_OUTPUT/workspace/main.go
+Range: L12:C6 - L12:C10
+- Calls: FooBar
+  Detail: github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace • main.go
+/TEST_OUTPUT/workspace/main.go
+  Range: L6:C6 - L6:C12
+- Calls: Println
+  Detail: fmt • print.go
+/GOROOT/src/fmt/print.go
+  Range: L313:C6 - L313:C13
diff --git a/integrationtests/snapshots/go/content/test_function.snap b/integrationtests/snapshots/go/content/test_function.snap
new file mode 100644
index 0000000..0782543
--- /dev/null
+++ b/integrationtests/snapshots/go/content/test_function.snap
@@ -0,0 +1,7 @@
+Symbol: TestFunction
+/TEST_OUTPUT/workspace/clean.go
+Range: L31:C1 - L33:C2
+
+31|func TestFunction() {
+32|	fmt.Println("This is a test function")
+33|}
diff --git a/integrationtests/snapshots/go/hover/struct-type.snap b/integrationtests/snapshots/go/hover/struct-type.snap
index 08c7820..34aa914 100644
--- a/integrationtests/snapshots/go/hover/struct-type.snap
+++ b/integrationtests/snapshots/go/hover/struct-type.snap
@@ -1,5 +1,5 @@
 ```go
-type SharedStruct struct { // size=56 (0x38)
+type SharedStruct struct { // size=56 (0x38), class=64 (0x40)
 	ID        int
 	Name      string
 	Value     float64
diff --git a/integrationtests/snapshots/python/definition/method.snap b/integrationtests/snapshots/python/definition/method.snap
index 00806d8..613ad34 100644
--- a/integrationtests/snapshots/python/definition/method.snap
+++ b/integrationtests/snapshots/python/definition/method.snap
@@ -4,21 +4,8 @@ Symbol: test_method
 /TEST_OUTPUT/workspace/main.py
 Kind: Method
 Container Name: TestClass
-Range: L18:C1 - L59:C22
+Range: L31:C1 - L41:C26
 
-18|class TestClass:
-19|    """A test class with methods and attributes."""
-20|
-21|    class_variable: str = "class variable"
-22|
-23|    def __init__(self, value: int = 0):
-24|        """Initialize the TestClass.
-25|
-26|        Args:
-27|            value: The initial value
-28|        """
-29|        self.value: int = value
-30|
 31|    def test_method(self, increment: int) -> int:
 32|        """Increment the value by the given amount.
 33|
@@ -30,22 +17,4 @@ Range: L18:C1 - L59:C22
 39|        """
 40|        self.value += increment
 41|        return self.value
-42|
-43|    @staticmethod
-44|    def static_method(items: list[str]) -> dict[str, int]:
-45|        """Convert a list of items to a dictionary with item counts.
-46|
-47|        Args:
-48|            items: A list of strings
-49|
-50|        Returns:
-51|            A dictionary mapping items to their counts
-52|        """
-53|        result: dict[str, int] = {}
-54|        for item in items:
-55|            if item in result:
-56|                result[item] += 1
-57|            else:
-58|                result[item] = 1
-59|        return result
 
diff --git a/integrationtests/snapshots/python/definition/static-method.snap b/integrationtests/snapshots/python/definition/static-method.snap
index d29faf4..b9e1ebe 100644
--- a/integrationtests/snapshots/python/definition/static-method.snap
+++ b/integrationtests/snapshots/python/definition/static-method.snap
@@ -4,33 +4,8 @@ Symbol: static_method
 /TEST_OUTPUT/workspace/main.py
 Kind: Method
 Container Name: TestClass
-Range: L18:C1 - L59:C22
+Range: L43:C1 - L59:C22
 
-18|class TestClass:
-19|    """A test class with methods and attributes."""
-20|
-21|    class_variable: str = "class variable"
-22|
-23|    def __init__(self, value: int = 0):
-24|        """Initialize the TestClass.
-25|
-26|        Args:
-27|            value: The initial value
-28|        """
-29|        self.value: int = value
-30|
-31|    def test_method(self, increment: int) -> int:
-32|        """Increment the value by the given amount.
-33|
-34|        Args:
-35|            increment: The amount to increment by
-36|
-37|        Returns:
-38|            The new value
-39|        """
-40|        self.value += increment
-41|        return self.value
-42|
 43|    @staticmethod
 44|    def static_method(items: list[str]) -> dict[str, int]:
 45|        """Convert a list of items to a dictionary with item counts.
diff --git a/integrationtests/snapshots/rust/definition/method.snap b/integrationtests/snapshots/rust/definition/method.snap
index b661954..dca91aa 100644
--- a/integrationtests/snapshots/rust/definition/method.snap
+++ b/integrationtests/snapshots/rust/definition/method.snap
@@ -4,21 +4,11 @@ Symbol: method
 /TEST_OUTPUT/workspace/src/types.rs
 Kind: Function
 Container Name: TestStruct
-Range: L18:C1 - L30:C2
+Range: L27:C1 - L29:C6
 
-18|// Implementation for TestStruct
-19|impl TestStruct {
-20|    pub fn new(name: &str, value: i32) -> Self {
-21|        TestStruct {
-22|            name: String::from(name),
-23|            value,
-24|        }
-25|    }
-26|
 27|    pub fn method(&self) -> String {
 28|        format!("{}: {}", self.name, self.value)
 29|    }
-30|}
 
 ---
 
@@ -26,17 +16,9 @@ Symbol: method
 /TEST_OUTPUT/workspace/src/types.rs
 Kind: Function
 Container Name: SharedStruct
-Range: L54:C1 - L64:C2
+Range: L61:C1 - L63:C6
 
-54|impl SharedStruct {
-55|    pub fn new(name: &str) -> Self {
-56|        SharedStruct {
-57|            name: String::from(name),
-58|        }
-59|    }
-60|
 61|    pub fn method(&self) -> String {
 62|        format!("SharedStruct: {}", self.name)
 63|    }
-64|}
 
diff --git a/integrationtests/snapshots/rust/hover/struct-type.snap b/integrationtests/snapshots/rust/hover/struct-type.snap
index ff0d961..cf8fd8e 100644
--- a/integrationtests/snapshots/rust/hover/struct-type.snap
+++ b/integrationtests/snapshots/rust/hover/struct-type.snap
@@ -6,7 +6,4 @@ pub struct TestStruct {
 }
 
 
-size = 32 (0x20), align = 0x8
-
-
-contain types with destructors (drop glue); doesn't have a destructor
\ No newline at end of file
+size = 32 (0x20), align = 0x8, needs Drop
\ No newline at end of file
diff --git a/integrationtests/tests/clangd/definition/definition_test.go b/integrationtests/tests/clangd/definition/definition_test.go
index 72d82d5..8ad6a46 100644
--- a/integrationtests/tests/clangd/definition/definition_test.go
+++ b/integrationtests/tests/clangd/definition/definition_test.go
@@ -65,6 +65,12 @@ func TestReadDefinition(t *testing.T) {
 			expectedText: "void method(int param)",
 			snapshotName: "method",
 		},
+		{
+			name:         "Namespace function",
+			symbolName:   "nsFunction2",
+			expectedText: "void nsFunction2()",
+			snapshotName: "nsFunction",
+		},
 		{
 			name:         "Struct",
 			symbolName:   "TestStruct",
diff --git a/integrationtests/tests/common/helpers.go b/integrationtests/tests/common/helpers.go
index 3d7243c..84ade98 100644
--- a/integrationtests/tests/common/helpers.go
+++ b/integrationtests/tests/common/helpers.go
@@ -1,9 +1,11 @@
 package common
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -91,10 +93,25 @@ func CleanupTestSuites(suites ...*TestSuite) {
 	}
 }
 
+// use instead of runtime.GOROOT which is deprecated
+func getGoRoot() string {
+	cmd := exec.Command("go", "env", "GOROOT")
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	if err != nil {
+		panic(err)
+	}
+	return strings.TrimSpace(out.String())
+}
+
 // normalizePaths replaces absolute paths in the result with placeholder paths for consistent snapshots
 func normalizePaths(_ *testing.T, input string) string {
 	// No need to get the repo root - we're just looking for patterns
 
+	// But this is useful
+	goroot := getGoRoot()
+
 	// Simple approach: just replace any path segments that contain workspace/
 	lines := strings.Split(input, "\n")
 	for i, line := range lines {
@@ -116,6 +133,13 @@ func normalizePaths(_ *testing.T, input string) string {
 				lines[i] = "/TEST_OUTPUT/workspace/" + parts[1]
 			}
 		}
+		if strings.Contains(line, goroot) {
+			parts := strings.Split(line, goroot)
+			if len(parts) > 1 {
+				// Replace with a simple placeholder path
+				lines[i] = "/GOROOT" + parts[1]
+			}
+		}
 	}
 
 	return strings.Join(lines, "\n")
diff --git a/integrationtests/tests/go/call_hierarchy/incoming_test.go b/integrationtests/tests/go/call_hierarchy/incoming_test.go
new file mode 100644
index 0000000..b0dc0f5
--- /dev/null
+++ b/integrationtests/tests/go/call_hierarchy/incoming_test.go
@@ -0,0 +1,58 @@
+package callhierarchy_test
+
+import (
+	"context"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
+	"github.com/isaacphi/mcp-language-server/integrationtests/tests/go/internal"
+	"github.com/isaacphi/mcp-language-server/internal/tools"
+)
+
+func TestIncomingCalls(t *testing.T) {
+	suite := internal.GetTestSuite(t)
+
+	ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
+	defer cancel()
+
+	tests := []struct {
+		name         string
+		symbolName   string
+		expectedText string
+		snapshotName string
+	}{
+		{
+			name:         "Function with calls in same file",
+			symbolName:   "FooBar",
+			expectedText: ": main",
+			snapshotName: "incoming-same-file",
+		},
+		{
+			name:         "Function with calls in other file",
+			symbolName:   "HelperFunction",
+			expectedText: "ConsumerFunction",
+			snapshotName: "incoming-other-file",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Call the GetIncomingCalls tool
+			result, err := tools.GetCallers(ctx, suite.Client, tc.symbolName, 1)
+			if err != nil {
+				t.Fatalf("Failed to find incoming calls: %v", err)
+			}
+
+			// Check that the result contains relevant information
+			if !strings.Contains(result, tc.expectedText) {
+				t.Errorf("Incoming calls do not contain expected text: %s", tc.expectedText)
+			}
+
+			// Use snapshot testing to verify exact output
+			common.SnapshotTest(t, "go", "call_hierarchy", tc.snapshotName, result)
+		})
+	}
+
+}
diff --git a/integrationtests/tests/go/call_hierarchy/outgoing_test.go b/integrationtests/tests/go/call_hierarchy/outgoing_test.go
new file mode 100644
index 0000000..d0cb745
--- /dev/null
+++ b/integrationtests/tests/go/call_hierarchy/outgoing_test.go
@@ -0,0 +1,58 @@
+package callhierarchy_test
+
+import (
+	"context"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
+	"github.com/isaacphi/mcp-language-server/integrationtests/tests/go/internal"
+	"github.com/isaacphi/mcp-language-server/internal/tools"
+)
+
+func TestOutgoingCalls(t *testing.T) {
+	suite := internal.GetTestSuite(t)
+
+	ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
+	defer cancel()
+
+	tests := []struct {
+		name         string
+		symbolName   string
+		expectedText string
+		snapshotName string
+	}{
+		{
+			name:         "Function with calls in other file",
+			symbolName:   "ConsumerFunction",
+			expectedText: "HelperFunction",
+			snapshotName: "outgoing-other-file",
+		},
+		{
+			name:         "Function with calls in same file",
+			symbolName:   "main",
+			expectedText: "FooBar",
+			snapshotName: "outgoing-same-file",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Call the GetOutgoingCalls tool
+			result, err := tools.GetCallees(ctx, suite.Client, tc.symbolName, 1)
+			if err != nil {
+				t.Fatalf("Failed to find outgoing calls: %v", err)
+			}
+
+			// Check that the result contains relevant information
+			if !strings.Contains(result, tc.expectedText) {
+				t.Errorf("Outgoing calls do not contain expected text: %s", tc.expectedText)
+			}
+
+			// Use snapshot testing to verify exact output
+			common.SnapshotTest(t, "go", "call_hierarchy", tc.snapshotName, result)
+		})
+	}
+
+}
diff --git a/integrationtests/tests/go/content/content_test.go b/integrationtests/tests/go/content/content_test.go
new file mode 100644
index 0000000..07c7a4d
--- /dev/null
+++ b/integrationtests/tests/go/content/content_test.go
@@ -0,0 +1,56 @@
+package content_test
+
+import (
+	"context"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
+	"github.com/isaacphi/mcp-language-server/integrationtests/tests/go/internal"
+	"github.com/isaacphi/mcp-language-server/internal/tools"
+)
+
+func TestContent(t *testing.T) {
+	suite := internal.GetTestSuite(t)
+
+	ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
+	defer cancel()
+
+	tests := []struct {
+		name         string
+		file         string
+		line         int
+		column       int
+		expectedText string
+		snapshotName string
+	}{
+		{
+			name:         "Function",
+			file:         filepath.Join(suite.WorkspaceDir, "clean.go"),
+			line:         32,
+			column:       1,
+			expectedText: "func TestFunction()",
+			snapshotName: "test_function",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Call the ReadDefinition tool
+			result, err := tools.GetContentInfo(ctx, suite.Client, tc.file, tc.line, tc.column)
+			if err != nil {
+				t.Fatalf("Failed to read content: %v", err)
+			}
+
+			// Check that the result contains relevant information
+			if !strings.Contains(result, tc.expectedText) {
+				t.Errorf("Content does not contain expected text: %s", tc.expectedText)
+			}
+
+			// Use snapshot testing to verify exact output
+			common.SnapshotTest(t, "go", "content", tc.snapshotName, result)
+		})
+	}
+}
diff --git a/integrationtests/tests/rust/internal/helpers.go b/integrationtests/tests/rust/internal/helpers.go
index 19cf38f..ba227c1 100644
--- a/integrationtests/tests/rust/internal/helpers.go
+++ b/integrationtests/tests/rust/internal/helpers.go
@@ -21,7 +21,7 @@ func GetTestSuite(t *testing.T) *common.TestSuite {
 		Command:          "rust-analyzer",
 		Args:             []string{},
 		WorkspaceDir:     filepath.Join(repoRoot, "integrationtests/workspaces/rust"),
-		InitializeTimeMs: 3000,
+		InitializeTimeMs: 5000,
 	}
 
 	// Create a test suite
diff --git a/integrationtests/workspaces/clangd/Makefile b/integrationtests/workspaces/clangd/Makefile
index dd96ef3..ce8f927 100644
--- a/integrationtests/workspaces/clangd/Makefile
+++ b/integrationtests/workspaces/clangd/Makefile
@@ -29,7 +29,7 @@ TARGET_CLEAN_PROGRAM = clean_program # Assuming this is another program to be bu
 # Listing them explicitly for clarity in target dependencies.
 OBJ_MAIN = $(OBJDIR)/main.o
 # Add other specific object files your 'program' executable depends on
-OTHER_OBJS = $(OBJDIR)/helper.o $(OBJDIR)/types.o $(OBJDIR)/consumer.o $(OBJDIR)/another_consumer.o
+OTHER_OBJS = $(OBJDIR)/helper.o $(OBJDIR)/types.o $(OBJDIR)/consumer.o $(OBJDIR)/another_consumer.o $(OBJDIR)/namespace.o
 OBJ_FOR_CLEAN_PROGRAM = $(OBJDIR)/clean.o
 
 
diff --git a/integrationtests/workspaces/clangd/src/namespace.cpp b/integrationtests/workspaces/clangd/src/namespace.cpp
new file mode 100644
index 0000000..cbadaa8
--- /dev/null
+++ b/integrationtests/workspaces/clangd/src/namespace.cpp
@@ -0,0 +1,15 @@
+//
+// A file with a namespace in it
+//
+
+namespace ns {
+
+void nsFunction1() {
+    // empty   
+}
+
+void nsFunction2() {
+    // empty   
+}
+
+}
\ No newline at end of file
diff --git a/internal/lsp/client.go b/internal/lsp/client.go
index fc07059..59ab58d 100644
--- a/internal/lsp/client.go
+++ b/internal/lsp/client.go
@@ -196,8 +196,8 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
 		return nil, fmt.Errorf("initialize failed: %w", err)
 	}
 
-	if err := c.Notify(ctx, "initialized", struct{}{}); err != nil {
-		return nil, fmt.Errorf("initialized notification failed: %w", err)
+	if err := c.Initialized(ctx, protocol.InitializedParams{}); err != nil {
+		return nil, fmt.Errorf("initialized failed: %w", err)
 	}
 
 	// Register handlers
@@ -208,12 +208,6 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
 	c.RegisterNotificationHandler("textDocument/publishDiagnostics",
 		func(params json.RawMessage) { HandleDiagnostics(c, params) })
 
-	// Notify the LSP server
-	err := c.Initialized(ctx, protocol.InitializedParams{})
-	if err != nil {
-		return nil, fmt.Errorf("initialization failed: %w", err)
-	}
-
 	// LSP sepecific Initialization
 	path := strings.ToLower(c.Cmd.Path)
 	switch {
diff --git a/internal/lsp/transport.go b/internal/lsp/transport.go
index 3cc9107..c2a3608 100644
--- a/internal/lsp/transport.go
+++ b/internal/lsp/transport.go
@@ -4,11 +4,13 @@ import (
 	"bufio"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"strings"
 
 	"github.com/isaacphi/mcp-language-server/internal/logging"
+	"github.com/isaacphi/mcp-language-server/internal/protocol"
 )
 
 // Create component-specific loggers
@@ -16,6 +18,11 @@ var lspLogger = logging.NewLogger(logging.LSP)
 var wireLogger = logging.NewLogger(logging.LSPWire)
 var processLogger = logging.NewLogger(logging.LSPProcess)
 
+var (
+	ErrContentModified = errors.New("content modified")
+	ErrServerCancelled = errors.New("server cancelled")
+)
+
 // WriteMessage writes an LSP message to the given writer
 func WriteMessage(w io.Writer, msg *Message) error {
 	data, err := json.Marshal(msg)
@@ -230,7 +237,14 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
 
 	if resp.Error != nil {
 		lspLogger.Error("Request failed: %s (code: %d)", resp.Error.Message, resp.Error.Code)
-		return fmt.Errorf("request failed: %s (code: %d)", resp.Error.Message, resp.Error.Code)
+		switch protocol.LSPErrorCodes(resp.Error.Code) {
+		case protocol.ContentModified:
+			return ErrContentModified
+		case protocol.ServerCancelled:
+			return ErrServerCancelled
+		default:
+			return fmt.Errorf("request failed: %s (code: %d)", resp.Error.Message, resp.Error.Code)
+		}
 	}
 
 	if result != nil {
diff --git a/internal/tools/call_hierarchy.go b/internal/tools/call_hierarchy.go
new file mode 100644
index 0000000..96d876d
--- /dev/null
+++ b/internal/tools/call_hierarchy.go
@@ -0,0 +1,200 @@
+package tools
+
+import (
+	"context"
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/isaacphi/mcp-language-server/internal/lsp"
+	"github.com/isaacphi/mcp-language-server/internal/protocol"
+)
+
+func GetCallers(ctx context.Context, client *lsp.Client, symbolName string, maxDepth int) (string, error) {
+	return getCallHierarchy(ctx, client, symbolName, maxDepth, recurseIncomingCalls)
+}
+
+func GetCallees(ctx context.Context, client *lsp.Client, symbolName string, maxDepth int) (string, error) {
+	return getCallHierarchy(ctx, client, symbolName, maxDepth, recurseOutgoingCalls)
+}
+
+func getCallHierarchy(
+	ctx context.Context, client *lsp.Client, symbolName string, maxDepth int,
+	recurse func(ctx context.Context, client *lsp.Client, item protocol.CallHierarchyItem, result *strings.Builder, depth int, maxDepth int),
+) (string, error) {
+	// First get the symbol location like ReadDefinition does
+	symbolName, results, err := QuerySymbol(ctx, client, symbolName)
+	if err != nil {
+		return "", err
+	}
+
+	// After this point we just return errors instead of erroring out
+	var result strings.Builder
+
+	for _, symbol := range results {
+		var separator string
+		if strings.Contains(symbolName, ".") {
+			separator = "."
+		} else if strings.Contains(symbolName, "::") {
+			separator = "::"
+		}
+
+		// Handle different matching strategies based on the search term
+		if separator != "" {
+			// For qualified names like "Type.Method", check for various matches
+			parts := strings.Split(symbolName, separator)
+			methodName := parts[len(parts)-1]
+
+			// Try matching the unqualified method name for languages that don't use qualified names in symbols
+			if symbol.GetName() != symbolName && symbol.GetName() != methodName {
+				continue
+			}
+		} else if symbol.GetName() != symbolName {
+			// For unqualified names, exact match only
+			continue
+		}
+
+		result.WriteString("\n---\n")
+
+		// Get the location of the symbol
+		loc := symbol.GetLocation()
+
+		chParams := protocol.CallHierarchyPrepareParams{
+			TextDocumentPositionParams: protocol.TextDocumentPositionParams{
+				TextDocument: protocol.TextDocumentIdentifier{
+					URI: loc.URI,
+				},
+				Position: loc.Range.Start,
+			},
+		}
+		items, err := client.PrepareCallHierarchy(ctx, chParams)
+		if err != nil {
+			result.WriteString(fmt.Sprintf("%s: Error: %v\n", symbol.GetName(), err))
+			continue
+		}
+
+		for _, item := range items {
+			recurse(ctx, client, item, &result, 0, maxDepth)
+		}
+	}
+
+	return result.String(), nil
+}
+
+func recurseIncomingCalls(ctx context.Context, client *lsp.Client, item protocol.CallHierarchyItem, result *strings.Builder, depth int, maxDepth int) {
+
+	var prefix string
+	if depth != 0 {
+		prefix = strings.Repeat(" ", (depth-1)*2+2)
+
+		result.WriteString(strings.Repeat(" ", (depth-1)*2))
+		result.WriteRune('-')
+		result.WriteString(" Called By: ")
+	} else {
+		result.WriteString("Name: ")
+	}
+
+	result.WriteString(item.Name)
+	result.WriteRune('\n')
+
+	result.WriteString(prefix)
+	result.WriteString("Detail: ")
+	result.WriteString(item.Detail)
+	result.WriteRune('\n')
+
+	result.WriteString(prefix)
+	result.WriteString("File: ")
+	result.WriteString(strings.TrimPrefix(string(item.URI), "file://"))
+	result.WriteRune('\n')
+
+	result.WriteString(prefix)
+	fmt.Fprintf(result, "Range: L%d:C%d - L%d:C%d\n",
+		item.Range.Start.Line+1,
+		item.Range.Start.Character+1,
+		item.Range.End.Line+1,
+		item.Range.End.Character+1)
+
+	if depth >= maxDepth {
+		return
+	}
+
+	calls, err := client.IncomingCalls(ctx, protocol.CallHierarchyIncomingCallsParams{
+		Item: item,
+	})
+
+	if err != nil {
+		result.WriteString(prefix)
+		result.WriteString("Error: ")
+		result.WriteString(err.Error())
+		result.WriteRune('\n')
+		return
+	}
+
+	// ensure output is deterministic for tests
+	sort.Slice(calls, func(i, j int) bool {
+		return calls[i].From.Name < calls[j].From.Name
+	})
+
+	for _, call := range calls {
+		recurseIncomingCalls(ctx, client, call.From, result, depth+1, maxDepth)
+	}
+}
+
+func recurseOutgoingCalls(ctx context.Context, client *lsp.Client, item protocol.CallHierarchyItem, result *strings.Builder, depth int, maxDepth int) {
+
+	var prefix string
+	if depth != 0 {
+		prefix = strings.Repeat(" ", (depth-1)*2+2)
+
+		result.WriteString(strings.Repeat(" ", (depth-1)*2))
+		result.WriteRune('-')
+		result.WriteString(" Calls: ")
+	} else {
+		result.WriteString("Name: ")
+	}
+
+	result.WriteString(item.Name)
+	result.WriteRune('\n')
+
+	result.WriteString(prefix)
+	result.WriteString("Detail: ")
+	result.WriteString(item.Detail)
+	result.WriteRune('\n')
+
+	result.WriteString(prefix)
+	result.WriteString("File: ")
+	result.WriteString(strings.TrimPrefix(string(item.URI), "file://"))
+	result.WriteRune('\n')
+
+	result.WriteString(prefix)
+	fmt.Fprintf(result, "Range: L%d:C%d - L%d:C%d\n",
+		item.Range.Start.Line+1,
+		item.Range.Start.Character+1,
+		item.Range.End.Line+1,
+		item.Range.End.Character+1)
+
+	if depth >= maxDepth {
+		return
+	}
+
+	calls, err := client.OutgoingCalls(ctx, protocol.CallHierarchyOutgoingCallsParams{
+		Item: item,
+	})
+
+	if err != nil {
+		result.WriteString(prefix)
+		result.WriteString("Error: ")
+		result.WriteString(err.Error())
+		result.WriteRune('\n')
+		return
+	}
+
+	// ensure output is deterministic for tests
+	sort.Slice(calls, func(i, j int) bool {
+		return calls[i].To.Name < calls[j].To.Name
+	})
+
+	for _, call := range calls {
+		recurseOutgoingCalls(ctx, client, call.To, result, depth+1, maxDepth)
+	}
+}
diff --git a/internal/tools/content.go b/internal/tools/content.go
new file mode 100644
index 0000000..7a50607
--- /dev/null
+++ b/internal/tools/content.go
@@ -0,0 +1,54 @@
+package tools
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/isaacphi/mcp-language-server/internal/lsp"
+	"github.com/isaacphi/mcp-language-server/internal/protocol"
+)
+
+// GetContentInfo reads the source code definition of a symbol (function, type, constant, etc.) at the specified position
+func GetContentInfo(ctx context.Context, client *lsp.Client, filePath string, line, column int) (string, error) {
+	// Open the file if not already open
+	err := client.OpenFile(ctx, filePath)
+	if err != nil {
+		return "", fmt.Errorf("could not open file: %v", err)
+	}
+
+	// Convert 1-indexed line/column to 0-indexed for LSP protocol
+	position := protocol.Position{
+		Line:      uint32(line - 1),
+		Character: uint32(column - 1),
+	}
+
+	location := protocol.Location{
+		URI: protocol.DocumentUri("file://" + filePath),
+		Range: protocol.Range{
+			Start: position,
+			End:   position,
+		},
+	}
+
+	definition, loc, symbol, err := GetFullDefinition(ctx, client, location)
+	if err != nil {
+		return "", err
+	}
+
+	locationInfo := fmt.Sprintf(
+		"Symbol: %s\n"+
+			"File: %s\n"+
+			"Range: L%d:C%d - L%d:C%d\n\n",
+		symbol.GetName(),
+		strings.TrimPrefix(string(loc.URI), "file://"),
+		loc.Range.Start.Line+1,
+		loc.Range.Start.Character+1,
+		loc.Range.End.Line+1,
+		loc.Range.End.Character+1,
+	)
+
+	definition = addLineNumbers(definition, int(loc.Range.Start.Line)+1)
+
+	return locationInfo + definition, nil
+}
diff --git a/internal/tools/definition.go b/internal/tools/definition.go
index 0ddd3fe..9595b49 100644
--- a/internal/tools/definition.go
+++ b/internal/tools/definition.go
@@ -10,16 +10,9 @@ import (
 )
 
 func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string) (string, error) {
-	symbolResult, err := client.Symbol(ctx, protocol.WorkspaceSymbolParams{
-		Query: symbolName,
-	})
+	symbolName, results, err := QuerySymbol(ctx, client, symbolName)
 	if err != nil {
-		return "", fmt.Errorf("failed to fetch symbol: %v", err)
-	}
-
-	results, err := symbolResult.Results()
-	if err != nil {
-		return "", fmt.Errorf("failed to parse results: %v", err)
+		return "", err
 	}
 
 	var definitions []string
@@ -28,33 +21,47 @@ func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string)
 		container := ""
 
 		// Skip symbols that we are not looking for. workspace/symbol may return
-		// a large number of fuzzy matches.
-		switch v := symbol.(type) {
-		case *protocol.SymbolInformation:
-			// SymbolInformation results have richer data.
-			kind = fmt.Sprintf("Kind: %s\n", protocol.TableKindMap[v.Kind])
-			if v.ContainerName != "" {
-				container = fmt.Sprintf("Container Name: %s\n", v.ContainerName)
+		// a large number of fuzzy matches. This handles BaseSymbolInformation
+		doesSymbolMatch := func(vKind protocol.SymbolKind, vContainerName string) bool {
+			thisName := symbol.GetName()
+
+			kind = fmt.Sprintf("Kind: %s\n", protocol.TableKindMap[vKind])
+			if vContainerName != "" {
+				container = fmt.Sprintf("Container Name: %s\n", vContainerName)
+			}
+
+			if thisName == symbolName {
+				return true
 			}
 
 			// Handle different matching strategies based on the search term
 			if strings.Contains(symbolName, ".") {
-				// For qualified names like "Type.Method", require exact match
-				if symbol.GetName() != symbolName {
-					continue
+				// For qualified names like "Type.Method", don't do fuzzy match
+
+			} else if vKind == protocol.Method {
+				// For methods, only match if the method name matches exactly Type.symbolName or Type::symbolName or symbolName
+				if strings.HasSuffix(thisName, "::"+symbolName) || strings.HasSuffix(symbolName, "::"+thisName) {
+					return true
 				}
-			} else {
-				// For unqualified names like "Method"
-				if v.Kind == protocol.Method {
-					// For methods, only match if the method name matches exactly Type.symbolName or Type::symbolName or symbolName
-					if !strings.HasSuffix(symbol.GetName(), "::"+symbolName) && !strings.HasSuffix(symbol.GetName(), "."+symbolName) && symbol.GetName() != symbolName {
-						continue
-					}
-				} else if symbol.GetName() != symbolName {
-					// For non-methods, exact match only
-					continue
+
+				if strings.HasSuffix(thisName, "."+symbolName) || strings.HasSuffix(symbolName, "."+thisName) {
+					return true
 				}
 			}
+
+			return false
+		}
+
+		switch v := symbol.(type) {
+		case *protocol.SymbolInformation:
+			if !doesSymbolMatch(v.Kind, v.ContainerName) {
+				continue
+			}
+
+		case *protocol.WorkspaceSymbol:
+			if !doesSymbolMatch(v.Kind, v.ContainerName) {
+				continue
+			}
 		default:
 			if symbol.GetName() != symbolName {
 				continue
@@ -71,7 +78,7 @@ func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string)
 		}
 
 		banner := "---\n\n"
-		definition, loc, err := GetFullDefinition(ctx, client, loc)
+		definition, loc, _, err := GetFullDefinition(ctx, client, loc)
 		locationInfo := fmt.Sprintf(
 			"Symbol: %s\n"+
 				"File: %s\n"+
diff --git a/internal/tools/hover.go b/internal/tools/hover.go
index 874d527..3c3dd53 100644
--- a/internal/tools/hover.go
+++ b/internal/tools/hover.go
@@ -2,6 +2,7 @@ package tools
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"strings"
 
@@ -31,9 +32,17 @@ func GetHoverInfo(ctx context.Context, client *lsp.Client, filePath string, line
 	params.Position = position
 
 	// Execute the hover request
-	hoverResult, err := client.Hover(ctx, params)
-	if err != nil {
-		return "", fmt.Errorf("failed to get hover information: %v", err)
+	// - some LSP (rust) will return "content modified", so retry it
+	var hoverResult protocol.Hover
+	for i := range 3 {
+		hoverResult, err = client.Hover(ctx, params)
+		if err == nil {
+			break
+		} else if errors.Is(err, lsp.ErrContentModified) {
+			continue
+		} else if i == 2 {
+			return "", fmt.Errorf("failed to get hover information: %v", err)
+		}
 	}
 
 	var result strings.Builder
diff --git a/internal/tools/lsp-utilities.go b/internal/tools/lsp-utilities.go
index ae7d70b..ccd6583 100644
--- a/internal/tools/lsp-utilities.go
+++ b/internal/tools/lsp-utilities.go
@@ -5,14 +5,19 @@ import (
 	"fmt"
 	"net/url"
 	"os"
+	"slices"
 	"strings"
 
 	"github.com/isaacphi/mcp-language-server/internal/lsp"
 	"github.com/isaacphi/mcp-language-server/internal/protocol"
 )
 
-// Gets the full code block surrounding the start of the input location
-func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation protocol.Location) (string, protocol.Location, error) {
+type match struct {
+	Symbol protocol.DocumentSymbolResult
+	Range  protocol.Range
+}
+
+func identifyOverlappingSymbols(ctx context.Context, client *lsp.Client, startLocation protocol.Location) ([]match, error) {
 	symParams := protocol.DocumentSymbolParams{
 		TextDocument: protocol.TextDocumentIdentifier{
 			URI: startLocation.URI,
@@ -22,54 +27,70 @@ func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation pr
 	// Get all symbols in document
 	symResult, err := client.DocumentSymbol(ctx, symParams)
 	if err != nil {
-		return "", protocol.Location{}, fmt.Errorf("failed to get document symbols: %w", err)
+		return nil, fmt.Errorf("failed to get document symbols: %w", err)
 	}
 
 	symbols, err := symResult.Results()
 	if err != nil {
-		return "", protocol.Location{}, fmt.Errorf("failed to process document symbols: %w", err)
+		return nil, fmt.Errorf("failed to process document symbols: %w", err)
 	}
 
-	var symbolRange protocol.Range
-	found := false
-
 	// Search for symbol at startLocation
-	var searchSymbols func(symbols []protocol.DocumentSymbolResult) bool
-	searchSymbols = func(symbols []protocol.DocumentSymbolResult) bool {
+	// - multiple symbols might match (for example, a C++ namespace) so find
+	//   all of the matching symbols and use the smallest one (or the first one
+	//   if there is a tie)
+	var matchingSymbols []match
+
+	var searchSymbols func(symbols []protocol.DocumentSymbolResult)
+	searchSymbols = func(symbols []protocol.DocumentSymbolResult) {
 		for _, sym := range symbols {
 			if containsPosition(sym.GetRange(), startLocation.Range.Start) {
-				symbolRange = sym.GetRange()
-				found = true
-				return true
+				matchingSymbols = append(matchingSymbols, match{sym, sym.GetRange()})
 			}
+
 			// Handle nested symbols if it's a DocumentSymbol
 			if ds, ok := sym.(*protocol.DocumentSymbol); ok && len(ds.Children) > 0 {
 				childSymbols := make([]protocol.DocumentSymbolResult, len(ds.Children))
 				for i := range ds.Children {
 					childSymbols[i] = &ds.Children[i]
 				}
-				if searchSymbols(childSymbols) {
-					return true
-				}
+				searchSymbols(childSymbols)
 			}
 		}
-		return false
 	}
 
-	found = searchSymbols(symbols)
+	searchSymbols(symbols)
+	return matchingSymbols, nil
+}
+
+// Gets the full code block surrounding the start of the input location
+func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation protocol.Location) (string, protocol.Location, protocol.DocumentSymbolResult, error) {
+
+	matchingSymbols, err := identifyOverlappingSymbols(ctx, client, startLocation)
+	if err != nil {
+		return "", protocol.Location{}, nil, err
+	}
+
+	// Identify the smallest overlapping symbol
+	slices.SortStableFunc(matchingSymbols, func(a, b match) int {
+		return int(a.Range.End.Line-a.Range.Start.Line) - int(b.Range.End.Line-b.Range.Start.Line)
+	})
+
+	if len(matchingSymbols) > 0 {
+		symbol := matchingSymbols[0].Symbol
+		symbolRange := matchingSymbols[0].Range
 
-	if found {
 		// Convert URI to filesystem path
 		filePath, err := url.PathUnescape(strings.TrimPrefix(string(startLocation.URI), "file://"))
 		if err != nil {
-			return "", protocol.Location{}, fmt.Errorf("failed to unescape URI: %w", err)
+			return "", protocol.Location{}, nil, fmt.Errorf("failed to unescape URI: %w", err)
 		}
 
 		// Read the file to get the full lines of the definition
 		// because we may have a start and end column
 		content, err := os.ReadFile(filePath)
 		if err != nil {
-			return "", protocol.Location{}, fmt.Errorf("failed to read file: %w", err)
+			return "", protocol.Location{}, nil, fmt.Errorf("failed to read file: %w", err)
 		}
 
 		lines := strings.Split(string(content), "\n")
@@ -79,7 +100,7 @@ func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation pr
 
 		// Get the line at the end of the range
 		if int(symbolRange.End.Line) >= len(lines) {
-			return "", protocol.Location{}, fmt.Errorf("line number out of range")
+			return "", protocol.Location{}, nil, fmt.Errorf("line number out of range")
 		}
 
 		line := lines[symbolRange.End.Line]
@@ -123,19 +144,16 @@ func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation pr
 			}
 		}
 
-		// Update location with new range
-		startLocation.Range = symbolRange
-
 		// Return the text within the range
 		if int(symbolRange.End.Line) >= len(lines) {
-			return "", protocol.Location{}, fmt.Errorf("end line out of range")
+			return "", protocol.Location{}, nil, fmt.Errorf("end line out of range")
 		}
 
 		selectedLines := lines[symbolRange.Start.Line : symbolRange.End.Line+1]
-		return strings.Join(selectedLines, "\n"), startLocation, nil
+		return strings.Join(selectedLines, "\n"), protocol.Location{URI: startLocation.URI, Range: symbolRange}, symbol, nil
 	}
 
-	return "", protocol.Location{}, fmt.Errorf("symbol not found")
+	return "", protocol.Location{}, nil, fmt.Errorf("symbol not found")
 }
 
 // GetLineRangesToDisplay determines which lines should be displayed for a set of locations
@@ -146,8 +164,8 @@ func GetLineRangesToDisplay(ctx context.Context, client *lsp.Client, locations [
 	// For each location, get its container and add relevant lines
 	for _, loc := range locations {
 		// Use GetFullDefinition to find container
-		_, containerLoc, err := GetFullDefinition(ctx, client, loc)
-		if err != nil {
+		matchingSymbols, _ := identifyOverlappingSymbols(ctx, client, loc)
+		if len(matchingSymbols) == 0 {
 			// If container not found, just use the location's line
 			refLine := int(loc.Range.Start.Line)
 			linesToShow[refLine] = true
@@ -161,9 +179,11 @@ func GetLineRangesToDisplay(ctx context.Context, client *lsp.Client, locations [
 			continue
 		}
 
+		containerRange := matchingSymbols[0].Range
+
 		// Add container start and end lines
-		containerStart := int(containerLoc.Range.Start.Line)
-		containerEnd := int(containerLoc.Range.End.Line)
+		containerStart := int(containerRange.Start.Line)
+		containerEnd := int(containerRange.End.Line)
 		linesToShow[containerStart] = true
 		// linesToShow[containerEnd] = true
 
diff --git a/internal/tools/references.go b/internal/tools/references.go
index cb424e5..6fe3f18 100644
--- a/internal/tools/references.go
+++ b/internal/tools/references.go
@@ -22,16 +22,9 @@ func FindReferences(ctx context.Context, client *lsp.Client, symbolName string)
 	}
 
 	// First get the symbol location like ReadDefinition does
-	symbolResult, err := client.Symbol(ctx, protocol.WorkspaceSymbolParams{
-		Query: symbolName,
-	})
+	symbolName, results, err := QuerySymbol(ctx, client, symbolName)
 	if err != nil {
-		return "", fmt.Errorf("failed to fetch symbol: %v", err)
-	}
-
-	results, err := symbolResult.Results()
-	if err != nil {
-		return "", fmt.Errorf("failed to parse results: %v", err)
+		return "", err
 	}
 
 	var allReferences []string
diff --git a/internal/tools/utilities.go b/internal/tools/utilities.go
index e5beb28..841d360 100644
--- a/internal/tools/utilities.go
+++ b/internal/tools/utilities.go
@@ -1,12 +1,14 @@
 package tools
 
 import (
+	"context"
 	"fmt"
 	"os"
 	"sort"
 	"strconv"
 	"strings"
 
+	"github.com/isaacphi/mcp-language-server/internal/lsp"
 	"github.com/isaacphi/mcp-language-server/internal/protocol"
 )
 
@@ -167,3 +169,33 @@ func FormatLinesWithRanges(lines []string, ranges []LineRange) string {
 
 	return result.String()
 }
+
+func doQuerySymbol(ctx context.Context, client *lsp.Client, symbolName string) ([]protocol.WorkspaceSymbolResult, error) {
+
+	symbolResult, err := client.Symbol(ctx, protocol.WorkspaceSymbolParams{
+		Query: symbolName,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch symbol: %v", err)
+	}
+
+	results, err := symbolResult.Results()
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse results: %v", err)
+	}
+	return results, nil
+}
+
+func QuerySymbol(ctx context.Context, client *lsp.Client, symbolName string) (string, []protocol.WorkspaceSymbolResult, error) {
+	results, err := doQuerySymbol(ctx, client, symbolName)
+
+	// clangd doesn't resolve "struct foo", only "foo"
+	if len(results) == 0 && strings.HasPrefix(symbolName, "struct ") {
+		results, err = doQuerySymbol(ctx, client, symbolName[7:])
+		if len(results) > 0 {
+			symbolName = symbolName[7:]
+		}
+	}
+
+	return symbolName, results, err
+}
diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go
index 6e7a0b8..bd405e8 100644
--- a/internal/watcher/watcher.go
+++ b/internal/watcher/watcher.go
@@ -456,27 +456,6 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
 
 	path = filepath.ToSlash(path)
 
-	// Special handling for wildcard patterns like "**/*"
-	if patternText == "**/*" {
-		// This should match any file
-		// watcherLogger.Debug("Using special matching for **/* pattern")
-		return true
-	}
-
-	// Special handling for wildcard patterns like "**/*.ext"
-	if strings.HasPrefix(patternText, "**/") {
-		if strings.HasPrefix(strings.TrimPrefix(patternText, "**/"), "*.") {
-			// Extension pattern like **/*.go
-			ext := strings.TrimPrefix(strings.TrimPrefix(patternText, "**/"), "*")
-			// watcherLogger.Debug("Using extension matching for **/*.ext pattern: checking if %s ends with %s", path, ext)
-			return strings.HasSuffix(path, ext)
-		} else {
-			// Any other pattern starting with **/ should match any path
-			// watcherLogger.Debug("Using path substring matching for **/ pattern")
-			return true
-		}
-	}
-
 	// For simple patterns without base path
 	if basePath == "" {
 		// Check if the pattern matches the full path or just the file extension
diff --git a/main.go b/main.go
index f6f3ed5..b51429c 100644
--- a/main.go
+++ b/main.go
@@ -8,9 +8,11 @@ import (
 	"os/exec"
 	"os/signal"
 	"path/filepath"
+	"strings"
 	"syscall"
 	"time"
 
+	"github.com/bmatcuk/doublestar/v4"
 	"github.com/isaacphi/mcp-language-server/internal/logging"
 	"github.com/isaacphi/mcp-language-server/internal/lsp"
 	"github.com/isaacphi/mcp-language-server/internal/watcher"
@@ -23,6 +25,7 @@ var coreLogger = logging.NewLogger(logging.Core)
 type config struct {
 	workspaceDir string
 	lspCommand   string
+	openGlobs    StringArrayFlag
 	lspArgs      []string
 }
 
@@ -35,10 +38,25 @@ type mcpServer struct {
 	workspaceWatcher *watcher.WorkspaceWatcher
 }
 
+// StringArrayFlag is a custom flag type to handle an array of strings
+type StringArrayFlag []string
+
+// Set appends a new value to the custom flag value
+func (s *StringArrayFlag) Set(value string) error {
+	*s = append(*s, value)
+	return nil
+}
+
+// String returns the string representation of the custom flag value
+func (s *StringArrayFlag) String() string {
+	return strings.Join(*s, ",")
+}
+
 func parseConfig() (*config, error) {
 	cfg := &config{}
 	flag.StringVar(&cfg.workspaceDir, "workspace", "", "Path to workspace directory")
 	flag.StringVar(&cfg.lspCommand, "lsp", "", "LSP command to run (args should be passed after --)")
+	flag.Var(&cfg.openGlobs, "open", "Glob of files to open by default (can specify more than once)")
 	flag.Parse()
 
 	// Get remaining args after -- as LSP arguments
@@ -99,10 +117,44 @@ func (s *mcpServer) initializeLSP() error {
 
 	coreLogger.Debug("Server capabilities: %+v", initResult.Capabilities)
 
+	if len(s.config.openGlobs) > 0 {
+		s.openInitialFiles()
+	}
+
 	go s.workspaceWatcher.WatchWorkspace(s.ctx, s.config.workspaceDir)
 	return client.WaitForServerReady(s.ctx)
 }
 
+func (s *mcpServer) openInitialFiles() {
+
+	err := filepath.WalkDir(s.config.workspaceDir, func(path string, d os.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if !d.IsDir() {
+			for _, pattern := range s.config.openGlobs {
+				match, err := doublestar.PathMatch(pattern, path)
+				if err != nil {
+					return err
+				}
+
+				if match {
+					if err := s.lspClient.OpenFile(s.ctx, path); err != nil {
+						coreLogger.Error("Failed to open file %s: %v", path, err)
+					}
+					break
+				}
+			}
+		}
+
+		return nil
+	})
+	if err != nil {
+		coreLogger.Error("openInitialFiles failed: %v", err)
+	}
+}
+
 func (s *mcpServer) start() error {
 	if err := s.initializeLSP(); err != nil {
 		return err
diff --git a/tools.go b/tools.go
index 55898ca..db38fd5 100644
--- a/tools.go
+++ b/tools.go
@@ -43,13 +43,13 @@ func (s *mcpServer) registerTools() error {
 
 	s.mcpServer.AddTool(applyTextEditTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 		// Extract arguments
-		filePath, ok := request.Params.Arguments["filePath"].(string)
-		if !ok {
-			return mcp.NewToolResultError("filePath must be a string"), nil
+		filePath, err := request.RequireString("filePath")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
 		// Extract edits array
-		editsArg, ok := request.Params.Arguments["edits"]
+		editsArg, ok := request.GetArguments()["edits"]
 		if !ok {
 			return mcp.NewToolResultError("edits is required"), nil
 		}
@@ -105,9 +105,9 @@ func (s *mcpServer) registerTools() error {
 
 	s.mcpServer.AddTool(readDefinitionTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 		// Extract arguments
-		symbolName, ok := request.Params.Arguments["symbolName"].(string)
-		if !ok {
-			return mcp.NewToolResultError("symbolName must be a string"), nil
+		symbolName, err := request.RequireString("symbolName")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
 		coreLogger.Debug("Executing definition for symbol: %s", symbolName)
@@ -129,9 +129,9 @@ func (s *mcpServer) registerTools() error {
 
 	s.mcpServer.AddTool(findReferencesTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 		// Extract arguments
-		symbolName, ok := request.Params.Arguments["symbolName"].(string)
-		if !ok {
-			return mcp.NewToolResultError("symbolName must be a string"), nil
+		symbolName, err := request.RequireString("symbolName")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
 		coreLogger.Debug("Executing references for symbol: %s", symbolName)
@@ -161,20 +161,13 @@ func (s *mcpServer) registerTools() error {
 
 	s.mcpServer.AddTool(getDiagnosticsTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 		// Extract arguments
-		filePath, ok := request.Params.Arguments["filePath"].(string)
-		if !ok {
-			return mcp.NewToolResultError("filePath must be a string"), nil
+		filePath, err := request.RequireString("filePath")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
-		contextLines := 5 // default value
-		if contextLinesArg, ok := request.Params.Arguments["contextLines"].(int); ok {
-			contextLines = contextLinesArg
-		}
-
-		showLineNumbers := true // default value
-		if showLineNumbersArg, ok := request.Params.Arguments["showLineNumbers"].(bool); ok {
-			showLineNumbers = showLineNumbersArg
-		}
+		contextLines := request.GetInt("contextLines", 5)
+		showLineNumbers := request.GetBool("showLineNumbers", true)
 
 		coreLogger.Debug("Executing diagnostics for file: %s", filePath)
 		text, err := tools.GetDiagnosticsForFile(s.ctx, s.lspClient, filePath, contextLines, showLineNumbers)
@@ -268,29 +261,19 @@ func (s *mcpServer) registerTools() error {
 
 	s.mcpServer.AddTool(hoverTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 		// Extract arguments
-		filePath, ok := request.Params.Arguments["filePath"].(string)
-		if !ok {
-			return mcp.NewToolResultError("filePath must be a string"), nil
+		filePath, err := request.RequireString("filePath")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
-		// Handle both float64 and int for line and column due to JSON parsing
-		var line, column int
-		switch v := request.Params.Arguments["line"].(type) {
-		case float64:
-			line = int(v)
-		case int:
-			line = v
-		default:
-			return mcp.NewToolResultError("line must be a number"), nil
+		line, err := request.RequireInt("line")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
-		switch v := request.Params.Arguments["column"].(type) {
-		case float64:
-			column = int(v)
-		case int:
-			column = v
-		default:
-			return mcp.NewToolResultError("column must be a number"), nil
+		column, err := request.RequireInt("column")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
 		coreLogger.Debug("Executing hover for file: %s line: %d column: %d", filePath, line, column)
@@ -324,34 +307,24 @@ func (s *mcpServer) registerTools() error {
 
 	s.mcpServer.AddTool(renameSymbolTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 		// Extract arguments
-		filePath, ok := request.Params.Arguments["filePath"].(string)
-		if !ok {
-			return mcp.NewToolResultError("filePath must be a string"), nil
+		filePath, err := request.RequireString("filePath")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
-		newName, ok := request.Params.Arguments["newName"].(string)
-		if !ok {
-			return mcp.NewToolResultError("newName must be a string"), nil
+		newName, err := request.RequireString("newName")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
-		// Handle both float64 and int for line and column due to JSON parsing
-		var line, column int
-		switch v := request.Params.Arguments["line"].(type) {
-		case float64:
-			line = int(v)
-		case int:
-			line = v
-		default:
-			return mcp.NewToolResultError("line must be a number"), nil
+		line, err := request.RequireInt("line")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
-		switch v := request.Params.Arguments["column"].(type) {
-		case float64:
-			column = int(v)
-		case int:
-			column = v
-		default:
-			return mcp.NewToolResultError("column must be a number"), nil
+		column, err := request.RequireInt("column")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
 		}
 
 		coreLogger.Debug("Executing rename_symbol for file: %s line: %d column: %d newName: %s", filePath, line, column, newName)
@@ -363,6 +336,94 @@ func (s *mcpServer) registerTools() error {
 		return mcp.NewToolResultText(text), nil
 	})
 
+	callersTool := mcp.NewTool("callers",
+		mcp.WithDescription("Determine which functions call the given symbol. Returns a list of the calling functions and the locations of the call sites."),
+		mcp.WithString("symbolName",
+			mcp.Required(),
+			mcp.Description("The name of the symbol whose callers you want to find (e.g. 'mypackage.MyFunction', 'MyType.MyMethod')"),
+		),
+	)
+	s.mcpServer.AddTool(callersTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+		// Extract arguments
+		symbolName, err := request.RequireString("symbolName")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
+		}
+
+		coreLogger.Debug("Executing callers for symbol: %s", symbolName)
+		text, err := tools.GetCallers(s.ctx, s.lspClient, symbolName, 1)
+		if err != nil {
+			coreLogger.Error("Failed to find callers: %v", err)
+			return mcp.NewToolResultError(fmt.Sprintf("failed to find callers: %v", err)), nil
+		}
+		return mcp.NewToolResultText(text), nil
+	})
+
+	calleesTool := mcp.NewTool("callees",
+		mcp.WithDescription("Resolve which functions a given symbol calls. Returns a list of the called functions and their locations."),
+		mcp.WithString("symbolName",
+			mcp.Required(),
+			mcp.Description("The name of the symbol whose callees you want to find (e.g. 'mypackage.MyFunction', 'MyType.MyMethod')"),
+		),
+	)
+	s.mcpServer.AddTool(calleesTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+		// Extract arguments
+		symbolName, err := request.RequireString("symbolName")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
+		}
+
+		coreLogger.Debug("Executing callees for symbol: %s", symbolName)
+		text, err := tools.GetCallees(s.ctx, s.lspClient, symbolName, 1)
+		if err != nil {
+			coreLogger.Error("Failed to find callees: %v", err)
+			return mcp.NewToolResultError(fmt.Sprintf("failed to find callees: %v", err)), nil
+		}
+		return mcp.NewToolResultText(text), nil
+	})
+
+	contentTool := mcp.NewTool("content",
+		mcp.WithDescription("Read the source code definition of a symbol (function, type, constant, etc.) at the specified location."),
+		mcp.WithString("filePath",
+			mcp.Required(),
+			mcp.Description("The path to the file"),
+		),
+		mcp.WithNumber("line",
+			mcp.Required(),
+			mcp.Description("The line number where the content is requested (1-indexed)"),
+		),
+		mcp.WithNumber("column",
+			mcp.Required(),
+			mcp.Description("The column number where the content is requested (1-indexed)"),
+		),
+	)
+
+	s.mcpServer.AddTool(contentTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+		// Extract arguments
+		filePath, err := request.RequireString("filePath")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
+		}
+
+		line, err := request.RequireInt("line")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
+		}
+
+		column, err := request.RequireInt("column")
+		if err != nil {
+			return mcp.NewToolResultError(err.Error()), nil
+		}
+
+		coreLogger.Debug("Executing content for file: %s line: %d column: %d", filePath, line, column)
+		text, err := tools.GetContentInfo(s.ctx, s.lspClient, filePath, line, column)
+		if err != nil {
+			coreLogger.Error("Failed to get content information: %v", err)
+			return mcp.NewToolResultError(fmt.Sprintf("failed to get content: %v", err)), nil
+		}
+		return mcp.NewToolResultText(text), nil
+	})
+
 	coreLogger.Info("Successfully registered all MCP tools")
 	return nil
 }