@@ -1138,8 +1138,18 @@ func (r *SourceResolver) resolveNpxCache(info ServerInfo) (*ResolvedSource, erro
11381138 }
11391139
11401140 // Search for the package in npx cache: ~/.npm/_npx/<hash>/node_modules/<package>/
1141+ //
1142+ // The SAME package name can appear under multiple npx cache hashes. One may
1143+ // hold the real installed source (package.json + dist/lib/*.js) while another
1144+ // holds only a tools.json stub that mcpproxy itself wrote into the cache when
1145+ // it dumped tool definitions. The stub's mtime is frequently NEWER than the
1146+ // real source (it was just written), so a naive newest-mtime pick selects the
1147+ // stub and the scan reports a false "1 file" coverage (MCP-2397). We therefore
1148+ // prefer candidates that look like real package source, and only fall back to
1149+ // the newest-mtime tiebreak among candidates of the same class.
11411150 var bestMatch string
11421151 var bestModTime int64
1152+ var bestReal bool
11431153
11441154 entries , err := os .ReadDir (npxCacheDir )
11451155 if err != nil {
@@ -1152,22 +1162,45 @@ func (r *SourceResolver) resolveNpxCache(info ServerInfo) (*ResolvedSource, erro
11521162 }
11531163 candidatePath := filepath .Join (npxCacheDir , entry .Name (), "node_modules" , pkgName )
11541164 stat , err := os .Stat (candidatePath )
1155- if err != nil {
1165+ if err != nil || ! stat . IsDir () {
11561166 continue
11571167 }
1158- if stat .IsDir () {
1159- modTime := stat .ModTime ().Unix ()
1160- if modTime > bestModTime {
1161- bestModTime = modTime
1162- bestMatch = candidatePath
1163- }
1168+ real := npxCacheLooksReal (candidatePath )
1169+ modTime := stat .ModTime ().Unix ()
1170+
1171+ // Selection order:
1172+ // 1. any real-source candidate beats any stub;
1173+ // 2. within the same class, the newest mtime wins.
1174+ better := false
1175+ switch {
1176+ case bestMatch == "" :
1177+ better = true
1178+ case real && ! bestReal :
1179+ better = true
1180+ case real == bestReal && modTime > bestModTime :
1181+ better = true
1182+ }
1183+ if better {
1184+ bestMatch = candidatePath
1185+ bestModTime = modTime
1186+ bestReal = real
11641187 }
11651188 }
11661189
11671190 if bestMatch == "" {
11681191 return nil , fmt .Errorf ("package %q not found in npx cache (%s)" , pkgName , npxCacheDir )
11691192 }
11701193
1194+ if ! bestReal {
1195+ // Every candidate looked like a stub. Scan it anyway (preserving prior
1196+ // behavior) but warn so the false-coverage case is visible in logs.
1197+ r .logger .Warn ("npx cache resolution found only stub directories (no package.json); scan coverage may be incomplete" ,
1198+ zap .String ("server" , info .Name ),
1199+ zap .String ("package" , pkgName ),
1200+ zap .String ("path" , bestMatch ),
1201+ )
1202+ }
1203+
11711204 r .logger .Info ("Resolved source from npx cache" ,
11721205 zap .String ("server" , info .Name ),
11731206 zap .String ("package" , pkgName ),
@@ -1181,6 +1214,38 @@ func (r *SourceResolver) resolveNpxCache(info ServerInfo) (*ResolvedSource, erro
11811214 }, nil
11821215}
11831216
1217+ // npxCacheLooksReal reports whether an npx cache package directory holds the
1218+ // real installed package rather than a bare stub. Every npm package ships a
1219+ // package.json, so its presence is the canonical marker of real source. A stub
1220+ // directory mcpproxy creates just to hold a dumped tools.json has none. As a
1221+ // secondary signal we also accept a directory that contains a conventional
1222+ // build/source subdirectory (dist/lib/build/src) or any JavaScript/TypeScript
1223+ // source file, in case an unusual package omits package.json at the cache root.
1224+ func npxCacheLooksReal (dir string ) bool {
1225+ if _ , err := os .Stat (filepath .Join (dir , "package.json" )); err == nil {
1226+ return true
1227+ }
1228+ for _ , sub := range []string {"dist" , "lib" , "build" , "src" } {
1229+ if stat , err := os .Stat (filepath .Join (dir , sub )); err == nil && stat .IsDir () {
1230+ return true
1231+ }
1232+ }
1233+ entries , err := os .ReadDir (dir )
1234+ if err != nil {
1235+ return false
1236+ }
1237+ for _ , e := range entries {
1238+ if e .IsDir () {
1239+ continue
1240+ }
1241+ switch strings .ToLower (filepath .Ext (e .Name ())) {
1242+ case ".js" , ".mjs" , ".cjs" , ".ts" :
1243+ return true
1244+ }
1245+ }
1246+ return false
1247+ }
1248+
11841249// resolveUvxCache finds a uvx package's source in ~/.cache/uv/ or ~/.local/share/uv/tools/
11851250func (r * SourceResolver ) resolveUvxCache (info ServerInfo ) (* ResolvedSource , error ) {
11861251 // Extract package name from args
0 commit comments