Project-Lens
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Project-LensWhich of my projects have uncommitted changes?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Project-Lens
A Model Context Protocol server that gives your AI coding agent a map of every project on your machine.
Coding agents are good at working inside one repository and blind outside of it. Ask one about a project in a sibling directory and it starts guessing paths, shelling out to find, and burning context on directory listings that mostly do not matter.
Project-Lens fixes that. It scans the workspace roots you configure, builds a registry of every version-controlled project it finds, and exposes eleven tools that let an agent answer questions like "where is the payments service?", "which of my repos are on a dirty branch?" or "which project defines TransactionManager?". Each in a single call, with responses shaped to be cheap in tokens rather than pretty to a human.
Design points worth knowing up front:
Stateless. Built on the MCP revision with the current
@modelcontextprotocol/serverv2 SDK, following its recommended server factory pattern. No session state is held between requests, so instances are interchangeable (scale them vertically or horizontally, restart them freely, run several side by side against different roots). Older 2025-era clients are still served through SDK version negotiation.Sandboxed by construction. Every path an agent supplies is resolved against a known project root;
../, absolute paths and symlinks that escape are rejected. Files matching secret patterns are invisible to every tool.Read-only by default. The two tools that mutate anything are not even registered unless you explicitly enable writes, so a confused or prompt-injected client cannot call what does not exist.
Token-frugal. Compact JSON, explicit truncation flags, and a positional
{fields, rows}matrix for bulk listings, one call to sweep a whole workspace instead of one call per project. Measured at 11.3× cheaper than the equivalent per-project calls.
Why it exists
The project started from a recurring annoyance: an agent that already had a perfectly good picture of the repository it was in had no idea that a dozen related repositories sat next to it. Every cross-project question turned into the same ritual (ls a few directories, guess at a name, find for a file, read a package.json to work out what the thing even is). Each step cost tokens, several steps returned nothing useful, and the agent still ended up asking which folder to look in.
The underlying problem is that a workspace has structure the filesystem does not express. You know that Work/Payments/Gateway is a service, that Work/Payments groups related services, and that the archived folder next to them is dead weight. An agent shelling out to find knows none of that and cannot learn it without paying for a directory walk every single time.
Project-Lens turns that structure into a queryable registry built once at startup, and puts a small, deliberately shaped tool surface in front of it:
One call per question. A whole-workspace metadata sweep is one
list_projectscall with a projection, not Nproject_infocalls.Responses sized for a context window. Positional rows instead of repeated keys, capped lists with an explicit
truncatedflag, per-line excerpt caps on search hits.A boundary the agent cannot cross. Reads and writes are scoped to a resolved project root, secrets are filtered out before anything is returned, and mutation is off unless you switch it on.
Related MCP server: agenticscope
How it works
A directory containing .git or .svn is a project. The folders between a configured root and that project become its group path, at arbitrary depth (Work/Payments/Gateway is a project named Gateway in group Work/Payments). Groups are whatever your directory layout already says they are; there is nothing to declare.
SVN working copies are indexed and visible, but the svn binary is never invoked, so their remote, branch and clean state stay null. Git projects get all three, queried live on the calls that report them.
Projects are addressed by a bare name (Gateway) when it is unique, or by the group-qualified path (Work/Payments/Gateway) when it is not. Matching is case-insensitive; an ambiguous name returns the qualified keys, a miss returns the closest matches.
Freshness: full scan at startup, an explicit refresh_registry tool, plus mtime revalidation of root and group directories piggybacked onto registry calls. No filesystem watcher, no disk cache, nothing to invalidate by hand.
Tool | Purpose |
| Tree overview: 1 = groups, 2 = groups → projects (default), 3 = + each project's top-level folders |
| Flat list of group paths |
| Projects as |
| Fuzzy name match → group + absolute path |
| Path, VCS remote/branch/clean state, detected stack, key manifests, README snippet |
| ripgrep content search over one project, one group, or every registered project; results grouped one entry per file |
| Paths relative to the project root; respects |
| Scoped read with optional line paging |
| Off by default. Scoped write; creates parent dirs inside the root |
| Off by default. User-initiated only. Create |
| Force a full re-scan, returns scan stats |
Narrowing a search. search only scans the directories of the projects in scope, never the whole root, and takes four parameters that narrow it further: path_prefix (a subdirectory of each project), glob (passed to rg -g), exclude (per-call pruning), and context_lines (0–3, default 0). The tool description steers the model toward these before widening to scope: "all".
Results come back grouped one entry per file, with hits as positional [line, excerpt] pairs:
{ "query": "TransactionManager", "scope": { "scope": "all" },
"files_matched": 12, "hits_returned": 34, "truncated": false,
"results": [
{ "project": "Gateway", "file": "src/tx/Manager.java",
"lines": [[14, "public class TransactionManager ..."], [88, " return new TransactionManager("]] }
] }Ripgrep output is read as a stream and stopped as soon as limit is reached, so a query matching a hundred thousand lines costs the same as one matching fifty.
Path traversal is rejected on every tool that takes a relative path
../, absolute paths and symlinks pointing outside the resolved project root.Secret-pattern files are invisible.
.env*,*.pem,id_rsa*and*credentials*never appear in key files, listings, search results orread_fileoutput.Remote URLs are sanitised.
project_infostrips userinfo (user:token@) from git remotes before returning them.Writes are unregistered, not merely refused, unless explicitly enabled a client that connects without them cannot call them at all.
Untrusted config cannot widen scope. A per-root
.project-lens.jsonmay only setexclude; a cloned repository cannot switch writes on or add roots.Failures are loud. Malformed configuration aborts startup rather than running with a silently changed scope.
Benchmarks
All figures come from one machine, one session, against a workspace of 42 projects across 16 groups with no exclusions configured and a pinned sample project. Your numbers will differ; reproduce them with npm run bench, npm run bench:store and npm run bench:tokens.
Read the two halves differently. Token counts are deterministic, the same workspace and sample give the same integers every run, so any movement is a real change. Latency is deterministic nowhere: project_info alone spans 12% across consecutive runs of identical code. Treat latency as a tripwire against the SLA column, not as evidence that one revision beat another.
npm run bench, 100 iterations per tool:
Tool | mean | p95 | SLA |
| 0.56 ms | 0.77 ms | < 15 ms |
| 0.47 ms | 0.51 ms | < 15 ms |
| 0.53 ms | 0.61 ms | < 20 ms |
| 0.51 ms | 0.58 ms | < 15 ms |
| 5.53 ms | 6.01 ms | < 100 ms |
| 6.63 ms | 7.21 ms | < 80 ms |
| 9.46 ms | 10.36 ms | < 80 ms |
| 71.64 ms | 79.57 ms | < 500 ms |
p99 is deliberately not reported: at 100 iterations the 99th percentile is the maximum, so printing both would show one number twice and read as though the second corroborated the first.
Every project-scoped row depends on which project got sampled, and that drift dwarfs the run-to-run noise. Unpinned, the harness takes whichever project sorts first by group-qualified key, so the sample moves as the workspace does. On the same machine at the same commit, search (one project) measured 6.63 ms against one sample and 7.54 ms against another enough to take the cost of widening to scope: "all" from 43% to 23%. Always pin:
LENS_BENCH_PROJECT="<group>/<Project>" npm run benchRegistry internals (npm run bench:store): cold scan ~86 ms, getAll() 0.11 µs, find() 3.6 µs, 10.5 MB heap used. Three caveats, because each is a different kind of measurement. The cold scan is a single sample that folds in OS page-cache state and module load, so it moves ±10 ms and is reported rounded. getAll() is timed as one 10 000-iteration batch and its result is consumed (per-iteration timing would have measured performance.now() itself, and an unconsumed result is free for the JIT to delete). find() is quoted as the median: its mean is 9.2 µs against a 3.6 µs median, because the first call carries JIT warm-up.
npm run bench:tokens, real claude-opus-5 counts. Each row compares one MCP call (request framing plus response) against the shell command a model would otherwise run for the same information, script text included.
Scenario | mcp | shell | ratio |
| 652 | 1275 | 0.51 |
| 207 | 359 | 0.58 |
| 124 | 210 | 0.59 |
| 3332 | 5221 | 0.64 ± |
| 255 | 375 | 0.68 |
| 989 | 978 | 1.01 |
| 391 | 382 | 1.02 |
| 155 | 109 | 1.42 |
| 1344 | 785 | 1.71 |
| 108 | 62 | 1.74 |
| 2263 | 1275 | 1.77 |
| 97 | 54 | 1.80 ± |
± marks the two rows that do not reproduce: both sides stop at 50 hits and ripgrep's traversal order picks which 50. Every other row is exact.
The headline number is not in that table. The metric that matters for bulk work is the projection: 1344 tokens for a 42-project metadata sweep against 15 173 for 42 individual project_info calls 11.3× smaller, past the ≥10× design target. The {fields, rows} matrix bought most of that; the array-of-objects shape it replaced scored 6.6×.
Rows above 1.0 are honest The list_projects rows stay above parity by design, the shell baselines print strictly less (find emits only paths; the sweep loop emits only branch plus porcelain status) while list_projects also carries name and group. The same asymmetry runs the other way in the strongest row: map_workspace is compared against a find emitting bare paths against a tool returning a tree, so part of its 0.51 is a discount for answering a smaller question. And the search (project) row found nothing at all (the pinned sample has no TODO) so its 1.80 is a fixed response envelope weighed against a command that printed nothing. It is published rather than deleted because dropping it would hide that the pinned sample cannot exercise that path.
Schema cost. All eleven tool schemas cost 2710 tokens, but a default session loads nine (write_file (298) and scaffold_project (254) are not registered unless writes are enabled) so the real one-time cost is 2158 tokens. Four map_workspace calls repay it. Repayment is quoted against the best stable scenario: search (scope: "all") saves far more, around 1900 tokens a call, but a headline built on it would be advertising precisely the unnarrowed call the tool's own description tells the model to avoid.
search is the most expensive schema at 658 tokens, a third of it the path_prefix, exclude and context_lines parameters (a one-time cost against a tool whose single unnarrowed call runs into the thousands, so those parameters pay for themselves the first time the model uses one). Every parameter of every tool carries a description, which is most of the difference between the nine-tool figure and a bare-schema server. It is worth the tokens: one read_file that guesses the project key wrong costs a rejected call plus a find_project round trip to recover.
One asymmetry is not corrected for: MCP is charged for its tool schemas and the shell is not. A model calling out to a shell pays for the Bash tool's schema too, and that cost never appears in the shell column. Every ratio here is therefore slightly pessimistic about MCP: the conservative direction, which is why it is left in.
Both shell baselines pass -M 300 --max-columns-preview, giving them the same per-line cap the tool applies to its excerpts. Both flags are load-bearing. -M 300 alone does not cap a long line, it drops it:
$ rg -n --no-heading -M 300 TODO vendor/bundle.min.js
2:[Omitted long matching line]That is 26 characters where the tool would have paid 300 (a silent subsidy to the shell on exactly the minified and vendored files the cap exists to handle). --max-columns-preview turns the omission back into a truncation, which is what the tool's excerpt cap actually does.
Some cap is non-negotiable: without one, a single minified line blows the comparison up, and the row swung between 1.1 MB and 17.9 MB depending on which lines head -50 happened to catch. Worth knowing as a property of the naive command (an uncapped rg | head -50 really can put megabytes into a context window).
Without an API token the harness counts UTF-8 bytes and labels itself DEGRADED. That error is a bias, not noise. Run back to back on the same workspace, bytes scored higher than tokens in all twelve rows, never once lower:
Scenario | tokens | bytes | |
| 1.01 | 1.02 | +1% |
| 1.02 | 1.04 | +2% |
| 0.68 | 0.70 | +3% |
| 1.77 | 1.86 | +5% |
| 0.64 | 0.71 | +11% |
| 1.42 | 1.57 | +11% |
| 1.74 | 2.04 | +17% |
| 0.59 | 0.71 | +20% |
The mechanism is visible in the ordering: the read_file rows are mostly prose and land within 2%, while rows dominated by paths, keys and JSON punctuation are off by 10–20%, because the tokenizer packs those far better than one byte per token and they are exactly what the MCP side of a row is made of. Bytes mode always flatters the shell. Good enough to spot a regression on a large payload, never a source for a published ratio.
Exclusions move every number here. Excluding a tree removes its projects from the registry entirely, which shrinks the startup scan, the candidate set for every search, and the size of every listing response but also makes those projects unresolvable by name from every tool, not just from search. On the benchmarked workspace, excluding one archive directory took list_projects from 42 rows to 14, the startup scan from 70 ms to 17 ms, and the candidate set for a TODO search from 465 files to 25. The configuration is part of the measurement, not context for it.
For users
Requirements
On the machine that runs the server:
Node.js ≥ 22
gitonPATHripgrep (
rg) onPATH, required bysearchandlist_files
The server exits loudly if any of these is missing; it never starts with a silently reduced tool surface.
Install
From a release asset (fastest, no build toolchain, no registry account):
npm install -g https://github.com/MatiasPujado/Project_Lens/releases/download/v0.3.0/mpujado-project-lens-0.3.0.tgznpm installs a tarball straight from a URL, so this needs no authentication and no registry configuration for the package itself. Its three runtime dependencies still resolve from whatever registry npm is already configured with. Check the releases page for the current version.
From source:
git clone https://github.com/MatiasPujado/Project_Lens.git && cd Project_Lens
npm ci
npm run build
npm link # optional: puts `project-lens` on PATHAs a tarball, for handing the built server to someone else:
npm run build && npm pack # writes mpujado-project-lens-<version>.tgz
npm install -g ./mpujado-project-lens-<version>.tgzThe package ships dist/src only (no tests, no benchmarks, no sources) and has three runtime dependencies (@modelcontextprotocol/server, picomatch, zod). Verify with which project-lens and npm ls -g @mpujado/project-lens; the binary takes no flags, so --version would be read as a workspace path and fail.
Point it at a workspace
The server needs one thing: a root directory to scan. Everything underneath it becomes groups and projects.
export PROJECT_LENS_PATH="$HOME/Projects"Precedence: positional CLI argument → PROJECT_LENS_PATH → roots in the config file. With none of them set, the server exits with an error. The root must exist and be a directory (~ is expanded by the server).
Register with an agent
Transport is stdio, so every client needs the same three things: a command to spawn, the workspace root, and optionally an environment block. Any spec-compliant MCP client can drive the server, only the registration syntax differs.
claude mcp add project-lens -- project-lens "$PROJECT_LENS_PATH"Or in a config file, where ${VAR} expansion is supported:
{
"mcpServers": {
"project-lens": {
"type": "stdio",
"command": "project-lens",
"args": ["${PROJECT_LENS_PATH}"],
"env": {}
}
}
}In ~/.codex/config.toml:
[mcp_servers.project-lens]
command = "project-lens"
args = ["/absolute/path/to/workspace"]In ~/.gemini/settings.json:
{
"mcpServers": {
"project-lens": {
"command": "project-lens",
"args": ["/absolute/path/to/workspace"]
}
}
}{
"mcpServers": {
"project-lens": {
"type": "stdio",
"command": "project-lens",
"args": ["/absolute/path/to/workspace"],
"env": {}
}
}
}Pick command to match how you installed:
Install |
|
Global ( |
|
Into a project |
|
Clone, without |
|
Two things that commonly trip people up:
GUI clients often do not inherit your shell
PATH, so a bare"project-lens"can fail to spawn even when it works in a terminal. Use an absolute path there (which project-lensprints it).Environment-variable expansion inside config JSON is client-specific. If yours does not support it, pass the workspace root literally in
args, or set it in the server's own environment:"env": {"PROJECT_LENS_PATH": "/absolute/path"}.
Only needed for exclusion globs, multiple roots, or enabling writes. Location: $PROJECTS_MCP_CONFIG if set, else $XDG_CONFIG_HOME/projects-mcp/config.json, else ~/.config/projects-mcp/config.json. A missing file is fine; a malformed one fails startup.
{
"roots": ["/absolute/path/to/workspace"],
"exclude": ["**/Archived*/**", "**/vendor/**"]
}Field | Type | Meaning |
|
| Absolute paths (or |
|
| Globs matched against root-relative paths; pruned before descent |
|
| Registers |
Excluding is worth doing: it shrinks the startup scan, the candidate set for every search, and every listing response. The tradeoff is real, though: an excluded tree leaves the registry entirely, so its projects stop being resolvable by name from every tool, not just from search.
A root can declare its own excludes in a .project-lens.json at its top level. This is the right place for them when the workspace is shared across machines, or when the server is launched with a bare path and no global config file at all:
{ "exclude": ["Archived/**"] }Its patterns are unioned with the global exclude, but only for that root. One root's excludes never apply to a sibling.
exclude is the only key this file may set. It lives inside a workspace tree, which is content an untrusted repository can author, so allow_writes and roots are ignored here on purpose: a cloned repository must not be able to turn the write tools on or widen the scan. Set those in the global config file only. A malformed .project-lens.json fails startup rather than being skipped.
write_file and scaffold_project are the only tools that mutate anything, and they are not registered unless you turn them on:
export PROJECT_LENS_ALLOW_WRITES=1 # or 'true' / 'yes' / 'on'{ "roots": ["/absolute/path/to/workspace"], "allow_writes": true }The environment variable wins over the config file. Everything else the server exposes is read-only.
For developers and contributors
src/ config, discovery, registry, git, search, security, stack, tools, server entry
test/ vitest suites, one per module, plus an in-process MCP integration suite
benchmark/ e2e latency, registry micro-benchmarks, token-cost harnessnpm run build # tsc
npm test # vitest: unit + integration
npm run test:watch # watch mode
npm run test:coverage # coverage report
npm run inspector # manual acceptance via the MCP InspectorSetup, testing, benchmarking, commit conventions and how to submit a change are in CONTRIBUTING.md.
License
See LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityCmaintenanceMCP server that gives AI agents deep code understanding across multiple git repositories, combining git lifecycle management, Zoekt-based trigram code search, and cross-repo dependency analysis.MIT
- Alicense-qualityAmaintenanceA read-only MCP server that provides AI agents with live, structured workspace awareness, including project listing, git status, and budgeted context packing, minimizing token usage.92MIT
- Flicense-qualityBmaintenanceWorkspace-aware MCP server that provides AI clients with structural code understanding via AST parsing, hybrid retrieval, and git history, enabling accurate code search, definition lookup, and blame analysis.
- Alicense-qualityAmaintenanceA production-ready MCP server that enables AI assistants to intelligently understand, analyze, edit, navigate, and review software projects with multi-workspace support, Git integration, and semantic search.MIT
Related MCP Connectors
A MCP server built for developers enabling Git based project management with project and personal…
An MCP server that gives your AI access to the source code and docs of all public github repos
MCP server for generating rough-draft project plans from natural-language prompts.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/MatiasPujado/Project_Lens'
If you have feedback or need assistance with the MCP directory API, please join our Discord server