portfolio-mcp
# portfolio-mcp
An MCP server that reads a directory full of projects and tells you what is in
there: what each one is built with, whether it is in git, where it is deployed,
what has gone stale, and which ones are closest to earning money.
It is read-only. It never writes to, moves, or deletes anything it scans.
## Install
```bash
npx portfolio-mcp --root /path/to/your/projects
```
Register it with Claude Code:
```bash
claude mcp add --scope user portfolio \
-e PORTFOLIO_ROOT=/path/to/your/projects \
-- npx -y portfolio-mcp
```
Or in any client that reads `mcpServers`:
```json
{
"mcpServers": {
"portfolio": {
"command": "npx",
"args": ["-y", "portfolio-mcp"],
"env": { "PORTFOLIO_ROOT": "/path/to/your/projects" }
}
}
}
```
## Configuration
Nothing is hardcoded. The scan root is resolved in this order:
1. `--root <path>` (or `--root=<path>`) on the command line
2. the `PORTFOLIO_ROOT` environment variable
3. the process working directory
| Variable | Default | Purpose |
|----------|---------|---------|
| `PORTFOLIO_ROOT` | working directory | Directory holding the projects to scan |
| `PORTFOLIO_VPS_HOST` | unset | Hostname or address that marks a self-hosted deployment. While unset, no `vps` platform is ever reported |
| `PORTFOLIO_OFFLINE` | unset | Set to `1` to refuse the one tool that reaches package registries |
`_archive`, `_backups`, `node_modules`, `.git`, and dotted directories are skipped.
## Tools
**Inventory**
| Tool | What it does |
|------|--------------|
| `list_projects` | Every project, filterable by stack, deployment status, platform, or GitHub presence |
| `get_project` | One project in depth: stack, dependencies, git, deployment, readiness, line count |
| `search_projects` | Match on name, description, framework, dependency, or category |
**Deployment**
| Tool | What it does |
|------|--------------|
| `deployment_map` | Everything grouped by platform: Vercel, Netlify, Railway, Docker, self-hosted, local-only |
| `check_health` | What deployment configuration one project carries, plus its git state |
| `git_overview` | Uncommitted work, missing remotes, repos stale past 90 days, directories with no git at all |
**Revenue**
| Tool | What it does |
|------|--------------|
| `revenue_scan` | Scores one project 1 to 10 on auth, payments, landing page, deployment, API, README |
| `find_opportunities` | Ranks every project, and separates the quick wins |
| `suggest_actions` | Concrete next steps for one project, in priority order |
| `learn_patterns` | Aggregate view: preferred frameworks, recurring stacks, strengths |
**Maintenance**
| Tool | What it does |
|------|--------------|
| `outdated_deps` | `npm outdated`, `npm audit`, and `pip list --outdated` for one project |
| `stale_projects` | Projects sorted by days since the last commit |
## What it touches
Being clear about this, because the server reads a directory you care about:
- **Reads only.** No tool writes, moves, renames, or deletes a file.
- **Path containment.** A project name is one directory entry. Separators, `..`,
absolute paths, drive letters, null bytes, and Windows device names are
rejected, and the resolved path is then checked to be inside the configured
root. There is no way to make it read a sibling directory.
- **No shell.** Every subprocess runs through `execFileSync` with an argument
array. Directory names are never concatenated into a command string.
- **Subprocesses.** `git` for repository state, and `npm` / `pip` only inside
`outdated_deps`.
- **Network.** `outdated_deps` alone reaches the npm and PyPI registries, because
that is what `npm outdated`, `npm audit`, and `pip list --outdated` do. Set
`PORTFOLIO_OFFLINE=1` and it refuses instead. Every other tool is local.
- **No credentials.** The server reads no tokens and sends nothing anywhere.
## Scoring
`revenue_scan` and `find_opportunities` produce a 1 to 10 score from signals that
are cheap to detect: deployment configuration, an auth library, a payment library,
a landing page entry point, a README, a git remote, an API framework, then a
category multiplier. It is a triage heuristic for sorting a large directory, not a
valuation. Treat the ranking as "look at these first", not as a number that means
anything on its own.
## Development
```bash
npm ci
npm run build
npm test # builds, then runs node --test
npm pack --dry-run
```
## License
MIT. See [LICENSE](LICENSE).
TDQS
Scored across 12 tools
Several tools have overlapping boundaries. git_overview and stale_projects both identify projects with no commits in 90+ days, and deployment_map and list_projects both provide project lists with deployment information. This creates real ambiguity for an agent choosing between them.
Most tools follow a clear verb_noun pattern like list_projects, get_project, search_projects, check_health, and find_opportunities. The exceptions are deployment_map and git_overview, which are noun phrases rather than verb-led names, but the naming remains largely predictable and readable.
Twelve tools is within a reasonable range for a portfolio analysis server and each tool serves a distinct functional area overall. The count is slightly higher than necessary due to redundant stale-project logic and the partial overlap between deployment_map and list_projects, but it is not excessive.
The tool surface covers project discovery, deep inspection, deployment status, health checks, git hygiene, dependency vulnerabilities, revenue analysis, and opportunity ranking. Minor gaps exist, such as no bulk dependency scan across all projects and no direct action/deploy tools, but these are workable within the server's apparent analysis-focused purpose.