goosy-mcp
v0.1.3
Published
Goosy MCP server — exposes Goosy's security-scan engine to AI coding agents over the Model Context Protocol.
Downloads
465
Maintainers
Readme
goosy-mcp
Goosy's security-scan engine, exposed to AI coding agents over the Model Context Protocol.
It is a thin client. There is no scanning logic, no rules, and no LLM calls in this repository — every tool call becomes an authenticated HTTPS request to the Goosy FastAPI resource server (a separate Python service). This package's entire job is: speak MCP on one side, speak Goosy's REST API on the other, and shape the results so they fit inside an agent's context window.
Implements docs/GOOSY_MCP_SERVER.md.
Install
// Claude Code / Claude Desktop / Cursor — MCP server config
{
"mcpServers": {
"goosy": {
"command": "npx",
"args": ["-y", "goosy-mcp"],
"env": { "GOOSY_API_BASE": "https://api.goosy.ai" }
}
}
}Then authorize once:
npx goosy-mcp login # opens a device-code flow in your browser
npx goosy-mcp doctor # verifies config, auth, and backend reachabilityYou can skip login — the first tool call returns an AUTH_REQUIRED error carrying the URL and code for the agent to relay to you. See Authentication.
The six tools
| Tool | What it does |
|---|---|
| goosy_scan_local | Bundles the working tree (including uncommitted files) and starts a scan. Returns a scan_id immediately. |
| goosy_scan_status | Polls state and progress. Carries retry_after_seconds so agents pace themselves. |
| goosy_list_findings | Paginated, summary-shaped findings with severity and path filters. |
| goosy_get_finding | Full detail for one finding: snippet, CWE, call path, remediation. |
| goosy_generate_fix | Returns a unified diff as text. The agent reviews and applies it. |
| goosy_explain_finding | Prose explanation with references, for justifying a change to a human. |
The design rule that matters most
Goosy returns data; the agent acts.
goosy_generate_fix returns a patch as text. This server never writes to your filesystem, never commits, and never opens a pull request. The agent already has file-editing tools and your trust to use them — supplying a diff keeps your existing review surface (the agent's own diff preview) intact.
v1 therefore deliberately exposes no write tools. There is no create_pr, no apply_patch, and no dismiss_finding: an agent in a loop must not be able to mutate a repository or silently suppress a security finding unattended. This is asserted mechanically in tests/tool-definitions.test.ts.
Layout
One module per responsibility. Each layer is testable without the one beneath it.
src/
├── index.ts CLI entry: serve (default), login, logout, whoami, doctor, tools
├── server.ts MCP wiring — stdio transport ↔ tool registry
├── config.ts Environment resolution, caps, timeouts
├── logger.ts stderr-only logging + token redaction + stdout guard
├── errors.ts Typed error envelope {code, message, retryable}
├── types.ts Domain and backend wire types
├── auth/
│ ├── device-flow.ts OAuth device grant against /api/platform/auth/*
│ ├── token-store.ts 0600 config.json, atomic writes, cross-process lock
│ └── auth-manager.ts AUTH_REQUIRED contract, refresh, grant coalescing
├── api/
│ ├── client.ts Bearer injection, retry/backoff, HTTP → error codes
│ └── endpoints.ts One method per backend route
├── bundler/
│ ├── path-guard.ts Workspace-root confinement (rejects /, ~, .., symlink escapes)
│ ├── deny-list.ts Secret deny-list: path patterns + content signatures
│ ├── git.ts HEAD sha and changed-file detection for scope:"diff"
│ ├── walker.ts Tree walk: gitignore, deny-list, caps, exclusion reporting
│ └── bundler.ts Streaming tar + zstd/gzip to a temp file
├── shaper/
│ ├── cursor.ts Opaque pagination cursors
│ └── result-shaper.ts Summary-first, bounded snippets, truncation disclosure
└── tools/
├── definitions.ts JSON Schemas (advertised) + zod validators (enforced)
├── registry.ts Validation, dispatch, error shaping
├── context.ts Handler dependency bundle
└── {scan-local, scan-status, list-findings,
get-finding, generate-fix, explain-finding}.tsTwo invariants worth knowing before you edit anything
stdout is the protocol. Any byte written to stdout that isn't a JSON-RPC message corrupts the session — this is the single most common way MCP servers break. All logging goes to stderr, and guardStdout() reroutes console.* so a transitive dependency's deprecation notice can't kill a session.
A failed scan is never reported as clean. goosy_scan_status surfaces failed as failed with findings_available: false, and goosy_list_findings refuses to return an empty array for a failed scan. The difference between "your code is clean" and "we didn't look" is the whole value of a security tool.
Authentication
MCP tool calls are request/response over stdio, but device-flow auth needs a human to open a browser — which can take a minute. A tool call cannot block that long, and this server has no UI channel of its own. Its only channel is the agent's transcript.
So the first call with no valid token returns a structured error, written for the model to relay:
{
"error": {
"code": "AUTH_REQUIRED",
"retryable": true,
"message": "Goosy needs authorization. Ask the user to open https://app.goosy.ai/device and enter code WXYZ-1234, then call this tool again.",
"verification_uri": "https://app.goosy.ai/device",
"user_code": "WXYZ-1234",
"expires_in": 900
}
}Polling continues in the background; the token is persisted on approval; the agent's retry just works. Concurrent tool calls join the same pending grant — issuing a second device code would show you a second number and invalidate the first.
Tokens are stored at ${XDG_CONFIG_HOME:-~/.config}/goosy/config.json (%APPDATA%\goosy\config.json on Windows), mode 0600, keyed by API base so production and a local stack can coexist. Refresh is transparent on 401 and serialized by a file lock so two processes can't race a rotation. Tokens are redacted from all log output.
Security model
| Risk | Control |
|---|---|
| Secrets uploaded from the working tree | Deny-list by path and by content signature, applied after .gitignore and overriding it — a git-tracked .env is still refused. The backend re-checks on ingest; this client is untrusted. |
| Agent steered to scan outside the project | Every path resolved through realpath and proven to sit under the workspace root. /, ~, .., and symlink escapes are rejected. |
| Unattended repo mutation | No write tool exists in v1. |
| Over-large bundles | Hard caps on file count, total bytes, and per-file size. Over-cap is an error with the actual numbers, never a silent truncation — a partial scan reported as complete is a false clean bill of health. |
| Runaway agent cost | retry_after_seconds on every pending response; server-side ceilings. |
Symlinks are never followed into a bundle. Every exclusion is counted and reported in the tool response's warnings.
The two gaps from the spec are now closed
Both lived in the backend, and both are fixed by platform/ in this repo:
- ~~MCP token scopes are not enforced server-side.~~ Every route in
platform/declares the scope it requires, andplatform/src/lib/auth.tsrefuses a token that lacks it. A token minted with["scan:read"]is now refused atpatch:writeandscan:writewith aFORBIDDENnaming both required and granted scopes. v1's safety property is no longer "the MCP server exposes no write tools" but the stronger "the token cannot perform writes" (backlog SEC-1). - ~~The
goosy_mcp_*token authenticates against nothing.~~platform/stores every credential as a SHA-256 hash and actually reads it on each request. The format from §2.3 is preserved and is now verifiable, with thescopesandexpiresAtthe old scaffold could not express (backlog AUTH-5).
Still open: session auth on /device/approve is a stub that fails closed — it must be pointed at a real session provider before deployment. See platform/README.md.
Development
npm install
npm run typecheck
npm test # 151 tests
npm run build
npm run dev # run from source via tsx
npm run dev:env # same, with .env loaded via node --env-fileThe test suite covers the spec's release gates directly: no write tool in tools/list, no secret in any bundle, path confinement, failed never reported as clean, truncation always disclosed, and the AUTH_REQUIRED coalescing contract.
Deployment
This package is not deployed as a service — it's a local stdio process an agent host spawns, published to npm and run via npx. The Dockerfile at the repo root exists only for CI/headless scanning (GOOSY_TOKEN + goosy-mcp doctor/scan), not hosting; see docs/DEPLOYMENT.md for why and how, and for deploying platform/ (the piece that actually runs as a server) to Vercel or Docker.
Environment
Copy .env.example to .env for the full list with defaults and commentary. Note the server does not read .env itself — there is no dotenv dependency, and loadConfig() reads process.env directly. In normal use these go in the env block of your MCP host config, because the host spawns the process rather than your shell.
| Variable | Default | Purpose |
|---|---|---|
| GOOSY_API_BASE | https://api.goosy.ai | Backend base URL |
| GOOSY_TOKEN | — | Pre-issued token for headless/CI; bypasses the device flow |
| GOOSY_WORKSPACE_ROOT | cwd | Directory scans are confined to |
| GOOSY_CONFIG | see above | Token store path override |
| GOOSY_LOG | text | json for structured logs (always on stderr) |
| GOOSY_LOG_LEVEL | info | debug | info | warn | error | silent |
| GOOSY_MAX_FILES | 20000 | Bundle file-count cap |
| GOOSY_MAX_TOTAL_BYTES | 104857600 | Bundle size cap |
Repository layout
| Directory | What it is |
|---|---|
| src/, tests/ | goosy-mcp — the npm-published MCP server. Runs locally, holds only a scoped token. |
| platform/ | goosy-platform — Next.js auth/API edge on Neon via Prisma. Issues and verifies tokens, enforces scopes, proxies scans. |
| (external) | The Python FastAPI scan engine. Not in this repo. |
Run the whole stack locally:
cd platform && npm install && npm run db:push && npm run db:seed && npm run dev
cd .. && GOOSY_API_BASE=http://localhost:3000 node dist/index.js loginBackend contract
This package expects these routes on GOOSY_API_BASE — served by platform/, see src/api/endpoints.ts:
POST /api/platform/auth/device/start | /device/poll | /refresh | /revoke
POST /api/v1/scans/local?mode=fast|deep (multipart bundle upload)
GET /api/v1/scans/{id}
GET /api/v1/scans/{id}/findings
GET /api/v1/findings/{id}
POST /api/v1/findings/{id}/patch
POST /api/v1/findings/{id}/explainResponse parsing is deliberately tolerant of key aliases (scan_id/id, file/file_path, patch/diff) so a backend rename doesn't break the agent loop silently.
#login system for powershell
$env:GOOSY_API_BASE = "http://localhost:3001"
node dist/index.js login
