This is page 1 of 2. Use http://codebase.md/pv-bhat/vibe-check-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .env.example
├── .github
│ └── workflows
│ ├── ci.yml
│ ├── docker-image.yml
│ └── release.yml
├── .gitignore
├── AGENTS.md
├── alt-test-gemini.js
├── alt-test-openai.js
├── alt-test.js
├── Attachments
│ ├── Template.md
│ ├── VC1.png
│ ├── vc2.png
│ ├── vc3.png
│ ├── vc4.png
│ ├── VCC1.png
│ ├── VCC2.png
│ ├── vibe (1).jpeg
│ ├── vibelogo.png
│ └── vibelogov2.png
├── CHANGELOG.md
├── CITATION.cff
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── docs
│ ├── _toc.md
│ ├── advanced-integration.md
│ ├── agent-prompting.md
│ ├── AGENTS.md
│ ├── api-keys.md
│ ├── architecture.md
│ ├── case-studies.md
│ ├── changelog.md
│ ├── clients.md
│ ├── docker-automation.md
│ ├── gemini.md
│ ├── integrations
│ │ └── cpi.md
│ ├── philosophy.md
│ ├── registry-descriptions.md
│ ├── release-workflows.md
│ ├── technical-reference.md
│ └── TESTING.md
├── examples
│ └── cpi-integration.ts
├── glama.json
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── request.json
├── scripts
│ ├── docker-setup.sh
│ ├── install-vibe-check.sh
│ ├── security-check.cjs
│ └── sync-version.mjs
├── SECURITY.md
├── server.json
├── smithery.yaml
├── src
│ ├── cli
│ │ ├── clients
│ │ │ ├── claude-code.ts
│ │ │ ├── claude.ts
│ │ │ ├── cursor.ts
│ │ │ ├── shared.ts
│ │ │ ├── vscode.ts
│ │ │ └── windsurf.ts
│ │ ├── diff.ts
│ │ ├── doctor.ts
│ │ ├── env.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── tools
│ │ ├── constitution.ts
│ │ ├── vibeCheck.ts
│ │ ├── vibeDistil.ts
│ │ └── vibeLearn.ts
│ └── utils
│ ├── anthropic.ts
│ ├── httpTransportWrapper.ts
│ ├── jsonRpcCompat.ts
│ ├── llm.ts
│ ├── state.ts
│ ├── storage.ts
│ └── version.ts
├── test-client.js
├── test-client.ts
├── test.js
├── test.json
├── tests
│ ├── claude-config.test.ts
│ ├── claude-merge.test.ts
│ ├── cli-doctor-node.test.ts
│ ├── cli-doctor-port.test.ts
│ ├── cli-install-dry-run.test.ts
│ ├── cli-install-vscode-dry-run.test.ts
│ ├── cli-start-flags.test.ts
│ ├── cli-version.test.ts
│ ├── constitution.test.ts
│ ├── cursor-merge.test.ts
│ ├── env-ensure.test.ts
│ ├── fixtures
│ │ ├── claude
│ │ │ ├── config.base.json
│ │ │ ├── config.with-managed-entry.json
│ │ │ └── config.with-other-servers.json
│ │ ├── cursor
│ │ │ ├── config.base.json
│ │ │ └── config.with-managed-entry.json
│ │ ├── vscode
│ │ │ ├── workspace.mcp.base.json
│ │ │ └── workspace.mcp.with-managed.json
│ │ └── windsurf
│ │ ├── config.base.json
│ │ ├── config.with-http-entry.json
│ │ └── config.with-managed-entry.json
│ ├── index-main.test.ts
│ ├── jsonrpc-compat.test.ts
│ ├── llm-anthropic.test.ts
│ ├── llm.test.ts
│ ├── server.integration.test.ts
│ ├── startup.test.ts
│ ├── state.test.ts
│ ├── storage-utils.test.ts
│ ├── vibeCheck.test.ts
│ ├── vibeLearn.test.ts
│ ├── vscode-merge.test.ts
│ └── windsurf-merge.test.ts
├── tsconfig.json
├── version.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | # Copy this file to .env and fill in your API key.
2 | GOOGLE_CLOUD_PROJECT="mcp-vibetest"
3 | DEFAULT_MODEL=gemini-2.5-flash
4 | DEFAULT_LLM_PROVIDER=gemini
5 | OPENAI_API_KEY=your_openai_key
6 | OPENROUTER_API_KEY=your_openrouter_key
7 | USE_LEARNING_HISTORY=false
8 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 | npm-debug.log
4 | yarn-debug.log
5 | yarn-error.log
6 |
7 | # Build output
8 | build/
9 | dist/
10 | *.tsbuildinfo
11 |
12 | # Environment variables
13 | .env
14 | .env.local
15 | .env.*.local
16 |
17 | # IDE and editor files
18 | .idea/
19 | .vscode/
20 | *.swp
21 | *.swo
22 | .DS_Store
23 |
24 | # Logs
25 | logs/
26 | *.log
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # Testing
32 | coverage/
33 | .nyc_output/
34 |
35 | # Temporary files
36 | tmp/
37 | temp/
38 |
39 | # Local configuration
40 | .npmrc
41 | .mcpregistry_github_token
42 | .mcpregistry_registry_token
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Vibe Check MCP
2 |
3 | <p align="center"><b>KISS overzealous agents goodbye. Plug & play agent oversight tool.</b></p>
4 |
5 | <p align="center">
6 | <b>Based on research:</b><br/>
7 | In our study agents calling Vibe Check improved success +27% and halved harmful actions -41%
8 | </p>
9 |
10 | <p align="center">
11 | <a href="https://www.researchgate.net/publication/394946231_Do_AI_Agents_Need_Mentors_Evaluating_Chain-Pattern_Interrupt_CPI_for_Oversight_and_Reliability?channel=doi&linkId=68ad6178ca495d76982ff192&showFulltext=true">
12 | <img src="https://img.shields.io/badge/Research-CPI%20%28MURST%29-blue?style=flat-square" alt="CPI Research">
13 | </a>
14 | <a href="https://github.com/modelcontextprotocol/servers"><img src="https://img.shields.io/badge/Anthropic%20MCP-featured-111?labelColor=111&color=555&style=flat-square" alt="Anthropic MCP: listed"></a>
15 | <a href="https://registry.modelcontextprotocol.io/"><img src="https://img.shields.io/badge/MCP%20Registry-listed-555?labelColor=111&style=flat-square" alt="MCP Registry"></a>
16 | <a href="https://www.pulsemcp.com/servers/pv-bhat-vibe-check">
17 | <img src="https://img.shields.io/badge/PulseMCP-Most%20Popular%20(Oct 2025)-0b7285?style=flat-square" alt="PulseMCP: Most Popular (this week)">
18 | </a>
19 | <a href="https://github.com/PV-Bhat/vibe-check-mcp-server/actions/workflows/ci.yml"><img src="https://github.com/PV-Bhat/vibe-check-mcp-server/actions/workflows/ci.yml/badge.svg" alt="CI passing"></a>
20 | <a href="https://smithery.ai/server/@PV-Bhat/vibe-check-mcp-server"><img src="https://smithery.ai/badge/@PV-Bhat/vibe-check-mcp-server" alt="Smithery Badge"></a>
21 | <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0b7285?style=flat-square" alt="MIT License"></a>
22 | </p>
23 |
24 | <p align="center">
25 | <sub> Featured on PulseMCP “Most Popular (This Week)” • 5k+ monthly calls on Smithery.ai • research-backed oversight • STDIO + streamable HTTP transport</sub>
26 | </p>
27 |
28 | <img width="500" height="300" alt="Gemini_Generated_Image_kvdvp4kvdvp4kvdv" src="https://github.com/user-attachments/assets/ff4d9efa-2142-436d-b1df-2a711a28c34e" />
29 |
30 | [](https://github.com/PV-Bhat/vibe-check-mcp-server)
31 | [](https://archestra.ai/mcp-catalog/pv-bhat__vibe-check-mcp-server)
32 | [](https://mseep.ai/app/a2954e62-a3f8-45b8-9a03-33add8b92599)
33 | [](CONTRIBUTING.md)
34 |
35 | *Plug-and-play mentor layer that stops agents from over-engineering and keeps them on the minimal viable path — research-backed MCP server keeping LLMs aligned, reflective and safe.*
36 |
37 | <div align="center">
38 | <a href="https://github.com/PV-Bhat/vibe-check-mcp-server">
39 | <img src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/github.svg" width="40" height="40" alt="GitHub" />
40 | </a>
41 |
42 | <a href="https://registry.modelcontextprotocol.io">
43 | <img src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/anthropic.svg" width="40" height="40" alt="Anthropic MCP Registry" />
44 | </a>
45 |
46 | <a href="https://smithery.ai/server/@PV-Bhat/vibe-check-mcp-server">
47 | <img src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/smithery.svg" width="40" height="40" alt="Smithery" />
48 | </a>
49 |
50 | <a href="https://www.pulsemcp.com/servers/pv-bhat-vibe-check">
51 | <img src="https://www.pulsemcp.com/favicon.ico" width="40" height="40" alt="PulseMCP" />
52 | </a>
53 | </div>
54 |
55 | <div align="center">
56 | <em>Trusted by developers across MCP platforms and registries</em>
57 | </div>
58 |
59 | ## Quickstart (npx)
60 |
61 | Run the server directly from npm without a local installation. Requires Node **>=20**. Choose a transport:
62 |
63 | ### Option 1 – MCP client over STDIO
64 |
65 | ```bash
66 | npx -y @pv-bhat/vibe-check-mcp start --stdio
67 | ```
68 |
69 | - Launch from an MCP-aware client (Claude Desktop, Cursor, Windsurf, etc.).
70 | - `[MCP] stdio transport connected` indicates the process is waiting for the client.
71 | - Add this block to your client config so it spawns the command:
72 |
73 | ```json
74 | {
75 | "mcpServers": {
76 | "vibe-check-mcp": {
77 | "command": "npx",
78 | "args": ["-y", "@pv-bhat/vibe-check-mcp", "start", "--stdio"]
79 | }
80 | }
81 | }
82 | ```
83 |
84 | ### Option 2 – Manual HTTP inspection
85 |
86 | ```bash
87 | npx -y @pv-bhat/vibe-check-mcp start --http --port 2091
88 | ```
89 |
90 | - `curl http://127.0.0.1:2091/health` to confirm the service is live.
91 | - Send JSON-RPC requests to `http://127.0.0.1:2091/rpc`.
92 |
93 | npx downloads the package on demand for both options. For detailed client setup and other commands like `install` and `doctor`, see the documentation below.
94 |
95 | [](https://www.star-history.com/#PV-Bhat/vibe-check-mcp-server&Date)
96 |
97 | ### Recognition
98 | - Featured on PulseMCP “Most Popular (This Week)” front page (week of 13 Oct 2025) [🔗](https://www.pulsemcp.com/servers/pv-bhat-vibe-check)
99 | - Listed in Anthropic’s official Model Context Protocol repo [🔗](https://github.com/modelcontextprotocol/servers?tab=readme-overview#-community-servers)
100 | - Discoverable in the official MCP Registry [🔗](https://registry.modelcontextprotocol.io/v0/servers?search=vibe-check-mcp)
101 | - Featured on Sean Kochel's Top 9 MCP servers for vibe coders [🔗](https://youtu.be/2wYO6sdQ9xc?si=mlVo4iHf_hPKghxc&t=1331)
102 |
103 | ## Table of Contents
104 | - [Quickstart (npx)](#quickstart-npx)
105 | - [What is Vibe Check MCP?](#what-is-vibe-check-mcp)
106 | - [Overview](#overview)
107 | - [The Problem: Pattern Inertia & Reasoning Lock-In](#the-problem-pattern-inertia--reasoning-lock-in)
108 | - [Key Features](#key-features)
109 | - [What's New](#whats-new-in-v274)
110 | - [Development Setup](#development-setup)
111 | - [Release](#release)
112 | - [Usage Examples](#usage-examples)
113 | - [Adaptive Metacognitive Interrupts (CPI)](#adaptive-metacognitive-interrupts-cpi)
114 | - [Agent Prompting Essentials](#agent-prompting-essentials)
115 | - [When to Use Each Tool](#when-to-use-each-tool)
116 | - [Documentation](#documentation)
117 | - [Research & Philosophy](#research--philosophy)
118 | - [Security](#security)
119 | - [Roadmap](#roadmap)
120 | - [Contributors & Community](#contributors--community)
121 | - [FAQ](#faq)
122 | - [Listed on](#find-vibe-check-mcp-on)
123 | - [Credits & License](#credits--license)
124 | ---
125 | ## What is Vibe Check MCP?
126 |
127 | Vibe Check MCP keeps agents on the minimal viable path and escalates complexity only when evidence demands it. Vibe Check MCP is a lightweight server implementing Anthropic's [Model Context Protocol](https://anthropic.com/mcp). It acts as an **AI meta-mentor** for your agents, interrupting pattern inertia with **Chain-Pattern Interrupts (CPI)** to prevent Reasoning Lock-In (RLI). Think of it as a rubber-duck debugger for LLMs – a quick sanity check before your agent goes down the wrong path.
128 |
129 | ## Overview
130 |
131 | Vibe Check MCP pairs a metacognitive signal layer with CPI so agents can pause when risk spikes. Vibe Check surfaces traits, uncertainty, and risk scores; CPI consumes those triggers and enforces an intervention policy before the agent resumes. See the [CPI integration guide](./docs/integrations/cpi.md) and the CPI repo at https://github.com/PV-Bhat/cpi for wiring details.
132 |
133 | Vibe Check invokes a second LLM to give meta-cognitive feedback to your main agent. Integrating vibe_check calls into agent system prompts and instructing tool calls before irreversible actions significantly improves agent alignment and common-sense. The high-level component map: [docs/architecture.md](./docs/architecture.md), while the CPI handoff diagram and example shim are captured in [docs/integrations/cpi.md](./docs/integrations/cpi.md).
134 |
135 | ## The Problem: Pattern Inertia & Reasoning Lock-In
136 |
137 | Large language models can confidently follow flawed plans. Without an external nudge they may spiral into overengineering or misalignment. Vibe Check provides that nudge through short reflective pauses, improving reliability and safety.
138 |
139 | ## Key Features
140 |
141 | | Feature | Description | Benefits |
142 | |---------|-------------|----------|
143 | | **CPI Adaptive Interrupts** | Phase-aware prompts that challenge assumptions | alignment, robustness |
144 | | **Multi-provider LLM** | Gemini, OpenAI, Anthropic, and OpenRouter support | flexibility |
145 | | **History Continuity** | Summarizes prior advice when `sessionId` is supplied | context retention |
146 | | **Optional vibe_learn** | Log mistakes and fixes for future reflection | self-improvement |
147 |
148 | ## What's New in v2.7.4
149 |
150 | - `install --client` now supports Cursor, Windsurf, and Visual Studio Code with idempotent merges, atomic writes, and `.bak` rollbacks.
151 | - HTTP-aware installers preserve `serverUrl` entries for Windsurf and emit VS Code workspace snippets plus a `vscode:mcp/install` link when no config is provided.
152 | - Documentation now consolidates provider keys, transport selection, uninstall guidance, and dedicated client docs at [docs/clients.md](./docs/clients.md).
153 |
154 | ## Session Constitution (per-session rules)
155 |
156 | Use a lightweight “constitution” to enforce rules per `sessionId` that CPI will honor. Eg. constitution rules: “no external network calls,” “prefer unit tests before refactors,” “never write secrets to disk.”
157 |
158 | **API (tools):**
159 | - `update_constitution({ sessionId, rules })` → merges/sets rule set for the session
160 | - `reset_constitution({ sessionId })` → clears session rules
161 | - `check_constitution({ sessionId })` → returns effective rules for the session
162 |
163 | ## Development Setup
164 | ```bash
165 | # Clone and install
166 | git clone https://github.com/PV-Bhat/vibe-check-mcp-server.git
167 | cd vibe-check-mcp-server
168 | npm ci
169 | npm run build
170 | npm test
171 | ```
172 | Use **npm** for all workflows (`npm ci`, `npm run build`, `npm test`). This project targets Node **>=20**.
173 |
174 | Create a `.env` file with the API keys you plan to use:
175 | ```bash
176 | # Gemini (default)
177 | GEMINI_API_KEY=your_gemini_api_key
178 | # Optional providers / Anthropic-compatible endpoints
179 | OPENAI_API_KEY=your_openai_api_key
180 | OPENROUTER_API_KEY=your_openrouter_api_key
181 | ANTHROPIC_API_KEY=your_anthropic_api_key
182 | ANTHROPIC_AUTH_TOKEN=your_proxy_bearer_token
183 | ANTHROPIC_BASE_URL=https://api.anthropic.com
184 | ANTHROPIC_VERSION=2023-06-01
185 | # Optional overrides
186 | # DEFAULT_LLM_PROVIDER accepts gemini | openai | openrouter | anthropic
187 | DEFAULT_LLM_PROVIDER=gemini
188 | DEFAULT_MODEL=gemini-2.5-pro
189 | ```
190 |
191 | #### Configuration
192 |
193 | See [docs/TESTING.md]() for instructions on how to run tests.
194 |
195 | ### Docker
196 | The repository includes a helper script for one-command setup.
197 | ```bash
198 | bash scripts/docker-setup.sh
199 | ```
200 | See [Automatic Docker Setup](./docs/docker-automation.md) for full details.
201 |
202 | ### Provider keys
203 |
204 | See [API Keys & Secret Management](./docs/api-keys.md) for supported providers, resolution order, storage locations, and security guidance.
205 |
206 | ### Transport selection
207 |
208 | The CLI supports stdio and HTTP transports. Transport resolution follows this order: explicit flags (`--stdio`/`--http`) → `MCP_TRANSPORT` → default `stdio`. When using HTTP, specify `--port` (or set `MCP_HTTP_PORT`); the default port is **2091**. The generated entries add `--stdio` or `--http --port <n>` accordingly, and HTTP-capable clients also receive a `http://127.0.0.1:<port>` endpoint.
209 |
210 | ### Client installers
211 |
212 | Each installer is idempotent and tags entries with `"managedBy": "vibe-check-mcp-cli"`. Backups are written once per run before changes are applied, and merges are atomic (`*.bak` files make rollback easy). See [docs/clients.md](./docs/clients.md) for deeper client-specific references.
213 |
214 | #### Claude Desktop
215 |
216 | - Config path: `claude_desktop_config.json` (auto-discovered per platform).
217 | - Default transport: stdio (`npx … start --stdio`).
218 | - Restart Claude Desktop after installation to load the new MCP server.
219 | - If an unmanaged entry already exists for `vibe-check-mcp`, the CLI leaves it untouched and prints a warning.
220 |
221 | #### Cursor
222 |
223 | - Config path: `~/.cursor/mcp.json` (provide `--config` if you store it elsewhere).
224 | - Schema mirrors Claude’s `mcpServers` layout.
225 | - If the file is missing, the CLI prints a ready-to-paste JSON block for Cursor’s settings panel instead of failing.
226 |
227 | #### Windsurf (Cascade)
228 |
229 | - Config path: legacy `~/.codeium/windsurf/mcp_config.json`, new builds use `~/.codeium/mcp_config.json`.
230 | - Pass `--http` to emit an entry with `serverUrl` for Windsurf’s HTTP client.
231 | - Existing sentinel-managed `serverUrl` entries are preserved and updated in place.
232 |
233 | #### Visual Studio Code
234 |
235 | - Workspace config lives at `.vscode/mcp.json`; profiles also store `mcp.json` in your VS Code user data directory.
236 | - Provide `--config <path>` to target a workspace file. Without `--config`, the CLI prints a JSON snippet and a `vscode:mcp/install?...` link you can open directly from the terminal.
237 | - VS Code supports optional dev fields; pass `--dev-watch` and/or `--dev-debug <value>` to populate `dev.watch`/`dev.debug`.
238 |
239 | ### Uninstall & rollback
240 |
241 | - Restore the backup generated during installation (the newest `*.bak` next to your config) to revert immediately.
242 | - To remove the server manually, delete the `vibe-check-mcp` entry under `mcpServers` (Claude/Windsurf/Cursor) or `servers` (VS Code) as long as it is still tagged with `"managedBy": "vibe-check-mcp-cli"`.
243 |
244 | ## Research & Philosophy
245 |
246 | **CPI (Chain-Pattern Interrupt)** is the research-backed oversight method behind Vibe Check. It injects brief, well-timed “pause points” at risk inflection moments to re-align the agent to the user’s true priority, preventing destructive cascades and **reasoning lock-in (RLI)**. In pooled evaluation across 153 runs, CPI **nearly doubles success (~27%→54%) and roughly halves harmful actions (~83%→42%)**. Optimal interrupt **dosage is ~10–20%** of steps. *Vibe Check MCP implements CPI as an external mentor layer at test time.*
247 |
248 | **Links:**
249 | - 📄 **CPI Paper (ResearchGate)** — http://dx.doi.org/10.13140/RG.2.2.18237.93922
250 | - 📘 **CPI Reference Implementation (GitHub)**: https://github.com/PV-Bhat/cpi
251 | - 📚 **MURST Zenodo DOI (RSRC archival)**: https://doi.org/10.5281/zenodo.14851363
252 |
253 | ```mermaid
254 | flowchart TD
255 | A[Agent Phase] --> B{Monitor Progress}
256 | B -- high risk --> C[CPI Interrupt]
257 | C --> D[Reflect & Adjust]
258 | B -- smooth --> E[Continue]
259 | ```
260 |
261 | ## Agent Prompting Essentials
262 | In your agent's system prompt, make it clear that `vibe_check` is a mandatory tool for reflection. Always pass the full user request and other relevant context. After correcting a mistake, you can optionally log it with `vibe_learn` to build a history for future analysis.
263 |
264 | Example snippet:
265 | ```
266 | As an autonomous agent you will:
267 | 1. Call vibe_check after planning and before major actions.
268 | 2. Provide the full user request and your current plan.
269 | 3. Optionally, record resolved issues with vibe_learn.
270 | ```
271 |
272 | ## When to Use Each Tool
273 | | Tool | Purpose |
274 | |------------------------|--------------------------------------------------------------|
275 | | 🛑 **vibe_check** | Challenge assumptions and prevent tunnel vision |
276 | | 🔄 **vibe_learn** | Capture mistakes, preferences, and successes |
277 | | 🧰 **update_constitution** | Set/merge session rules the CPI layer will enforce |
278 | | 🧹 **reset_constitution** | Clear rules for a session |
279 | | 🔎 **check_constitution** | Inspect effective rules for a session |
280 |
281 | ## Documentation
282 | - [Agent Prompting Strategies](./docs/agent-prompting.md)
283 | - [CPI Integration](./docs/integrations/cpi.md)
284 | - [Advanced Integration](./docs/advanced-integration.md)
285 | - [Technical Reference](./docs/technical-reference.md)
286 | - [Automatic Docker Setup](./docs/docker-automation.md)
287 | - [Philosophy](./docs/philosophy.md)
288 | - [Case Studies](./docs/case-studies.md)
289 | - [Changelog](./docs/changelog.md)
290 |
291 | ## Security
292 | This repository includes a CI-based security scan that runs on every pull request. It checks dependencies with `npm audit` and scans the source for risky patterns. See [SECURITY.md](./SECURITY.md) for details and how to report issues.
293 |
294 | ## Roadmap (New PRs welcome)
295 |
296 | ### Priority 1 – Builder Experience & Guidance
297 | - **Structured output for `vibe_check`:** Return a JSON envelope such as `{ advice, riskScore, traits }` so downstream agents can reason deterministically while preserving readable reflections.
298 | - **Agent prompt starter kit:** Publish a plug-and-play system prompt snippet that teaches the CPI dosage principle (10–20% of steps), calls out risk inflection points, and reminds agents to include the last 5–10 tool calls in `taskContext`.
299 | - **Documentation refresh:** Highlight the new prompt template and context requirements throughout the README and integration guides.
300 |
301 | ### Priority 2 – Core Reliability Requests
302 | - **LLM resilience:** Wrap `generateResponse` in `src/utils/llm.ts` with retries and exponential backoff, with a follow-up circuit breaker once the basics land.
303 | - **Input sanitization:** Validate and cleanse tool arguments in `src/index.ts` to mitigate prompt-injection vectors.
304 | - **State stewardship:** Add TTL-based cleanup in `src/utils/state.ts` and switch `src/utils/storage.ts` file writes to `fs.promises` to avoid blocking the event loop.
305 |
306 | These initiatives are tracked as community-facing GitHub issues so contributors can grab them and see progress in the open.
307 |
308 | ### Additional Follow-On Ideas & Good First Issues
309 | - **Telemetry sanity checks:** Add a lint-style CI step that verifies `docs/` examples compile (e.g., TypeScript snippet type-check) to catch drift between docs and code.
310 | - **CLI help polish:** Ensure every CLI subcommand prints a concise `--help` example aligned with the refreshed prompt guidance.
311 | - **Docs navigation cleanup:** Cross-link `docs/agent-prompting.md` and `docs/technical-reference.md` from the README section headers to reduce context switching for new contributors.
312 |
313 | ## Contributors & Community
314 | Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md).
315 |
316 | <a href="https://github.com/PV-Bhat/vibe-check-mcp-server/graphs/contributors">
317 | <img src="https://contrib.rocks/image?repo=PV-Bhat/vibe-check-mcp-server" alt="Contributors"/>
318 | </a>
319 |
320 | ## Links
321 | * [MSEEP](https://mseep.ai/app/pv-bhat-vibe-check-mcp-server)
322 | * [MCP Servers](https://mcpservers.org/servers/PV-Bhat/vibe-check-mcp-server)
323 | * [MCP.so](https://mcp.so/server/vibe-check-mcp-server/PV-Bhat)
324 | * [Creati.ai](https://creati.ai/mcp/vibe-check-mcp-server/)
325 | * [Pulse MCP](https://www.pulsemcp.com/servers/pv-bhat-vibe-check)
326 | * [Playbooks.com](https://playbooks.com/mcp/pv-bhat-vibe-check)
327 | * [MCPHub.tools](https://mcphub.tools/detail/PV-Bhat/vibe-check-mcp-server)
328 | * [MCP Directory](https://mcpdirectory.ai/mcpserver/2419/)
329 |
330 | ## Credits & License
331 | Vibe Check MCP is released under the [MIT License](LICENSE). Built for reliable, enterprise-ready AI agents.
332 |
333 | ## Author Credits & Links
334 | Vibe Check MCP created by: [Pruthvi Bhat](https://pruthvibhat.com/), Initiative - https://murst.org/
335 |
```
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
```markdown
1 | # Code of Conduct
2 |
3 | This project adheres to the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) Code of Conduct. By participating, you are expected to uphold this code.
4 |
5 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainer listed in `package.json`.
6 |
```
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
```markdown
1 | # Security Policy
2 |
3 | VibeCheck MCP is designed as a lightweight oversight layer for AI coding agents. While it does not execute code on behalf of the agent, it processes user prompts and sends them to third‑party LLM APIs. This document outlines our approach to keeping that process secure.
4 |
5 | ## Supported Versions
6 | Only the latest release receives security updates. Please upgrade regularly to stay protected.
7 |
8 | ## Threat Model
9 | - **Prompt injection**: malicious text could attempt to alter the meta-mentor instructions. VibeCheck uses a fixed system prompt and validates required fields to mitigate this.
10 | - **Tool misuse**: the server exposes only two safe tools (`vibe_check` and `vibe_learn`). No command execution or file access is performed.
11 | - **Data leakage**: requests are forwarded to the configured LLM provider. Avoid sending sensitive data if using hosted APIs. The optional `vibe_learn` log can be disabled via environment variables.
12 | - **Impersonation**: run VibeCheck only from this official repository or the published npm package. Verify the source before deployment.
13 |
14 | ## Reporting a Vulnerability
15 | If you discover a security issue, please open a private GitHub issue or email the maintainer listed in `package.json`. We will acknowledge your report within 48 hours and aim to provide a fix promptly.
16 |
17 | ## Continuous Security
18 | A custom security scan runs in CI on every pull request. It checks dependencies for known vulnerabilities and searches the source tree for dangerous patterns. The workflow fails if any issue is detected.
19 |
20 |
```
--------------------------------------------------------------------------------
/AGENTS.md:
--------------------------------------------------------------------------------
```markdown
1 | # Agent Quickstart
2 |
3 | Vibe Check MCP is a lightweight oversight layer for AI agents. It exposes two tools:
4 |
5 | - **vibe_check** – prompts you with clarifying questions to prevent tunnel vision.
6 | - **vibe_learn** – optional logging of mistakes and successes for later review.
7 |
8 | The server supports Gemini, OpenAI, Anthropic, and OpenRouter LLMs. History is maintained across requests when a `sessionId` is provided.
9 |
10 | ## Setup
11 |
12 | 1. Install dependencies and build:
13 | ```bash
14 | npm install
15 | npm run build
16 | ```
17 | 2. Supply the following environment variables as needed:
18 | - `GEMINI_API_KEY`
19 | - `OPENAI_API_KEY`
20 | - `OPENROUTER_API_KEY`
21 | - `ANTHROPIC_API_KEY` *(official Anthropic deployments)*
22 | - `ANTHROPIC_AUTH_TOKEN` *(Anthropic-compatible proxies)*
23 | - `ANTHROPIC_BASE_URL` *(optional; defaults to https://api.anthropic.com)*
24 | - `ANTHROPIC_VERSION` *(optional; defaults to 2023-06-01)*
25 | - `DEFAULT_LLM_PROVIDER` (gemini | openai | openrouter | anthropic)
26 | - `DEFAULT_MODEL` (e.g., gemini-2.5-pro)
27 | 3. Start the server:
28 | ```bash
29 | npm start
30 | ```
31 |
32 | ## Testing
33 |
34 | Run unit tests with `npm test`. Example request generators are provided:
35 |
36 | - `alt-test-gemini.js`
37 | - `alt-test-openai.js`
38 | - `alt-test.js` (OpenRouter)
39 |
40 | Each script writes a `request.json` file that you can pipe to the server:
41 |
42 | ```bash
43 | node build/index.js < request.json
44 | ```
45 |
46 | ## Integration Tips
47 |
48 | Call `vibe_check` regularly with your goal, plan and current progress. Use `vibe_learn` whenever you want to record a resolved issue. Full API details are in `docs/technical-reference.md`.
49 |
```
--------------------------------------------------------------------------------
/docs/AGENTS.md:
--------------------------------------------------------------------------------
```markdown
1 | # Agent Quickstart
2 |
3 | Vibe Check MCP is a lightweight oversight layer for AI agents. It exposes two tools:
4 |
5 | - **vibe_check** – prompts you with clarifying questions to prevent tunnel vision.
6 | - **vibe_learn** – optional logging of mistakes and successes for later review.
7 |
8 | The server supports Gemini, OpenAI, Anthropic, and OpenRouter LLMs. History is maintained across requests when a `sessionId` is provided.
9 |
10 | ## Setup
11 |
12 | 1. Install dependencies and build:
13 | ```bash
14 | npm install
15 | npm run build
16 | ```
17 | 2. Supply the following environment variables as needed:
18 | - `GEMINI_API_KEY`
19 | - `OPENAI_API_KEY`
20 | - `OPENROUTER_API_KEY`
21 | - `ANTHROPIC_API_KEY` *(official Anthropic deployments)*
22 | - `ANTHROPIC_AUTH_TOKEN` *(Anthropic-compatible proxies)*
23 | - `ANTHROPIC_BASE_URL` *(optional; defaults to https://api.anthropic.com)*
24 | - `ANTHROPIC_VERSION` *(optional; defaults to 2023-06-01)*
25 | - `DEFAULT_LLM_PROVIDER` (gemini | openai | openrouter | anthropic)
26 | - `DEFAULT_MODEL` (e.g., gemini-2.5-pro)
27 | 3. Start the server:
28 | ```bash
29 | npm start
30 | ```
31 |
32 | ## Testing
33 |
34 | Run unit tests with `npm test`. The JSON-RPC compatibility shim mitigates missing `id` fields on `tools/call` requests so the standard SDK client and Windsurf work without extra tooling, but compliant clients should continue to send their own identifiers. Example request generators remain available if you want canned payloads for manual testing:
35 |
36 | - `alt-test-gemini.js`
37 | - `alt-test-openai.js`
38 | - `alt-test.js` (OpenRouter)
39 |
40 | Each script writes a `request.json` file that you can pipe to the server:
41 |
42 | ```bash
43 | node build/index.js < request.json
44 | ```
45 |
46 | ## Integration Tips
47 |
48 | Call `vibe_check` regularly with your goal, plan and current progress. Use `vibe_learn` whenever you want to record a resolved issue. Full API details are in `docs/technical-reference.md`.
49 |
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributing to Vibe Check MCP
2 |
3 | First off, thanks for considering contributing to Vibe Check! It's people like you that help make this metacognitive oversight layer even better.
4 |
5 | ## The Vibe of Contributing
6 |
7 | Contributing to Vibe Check isn't just about code—it's about joining a community that's trying to make AI agents a bit more self-aware (since they're not quite there yet on their own).
8 |
9 | ### The Basic Flow
10 |
11 | 1. **Find something to improve**: Did your agent recently go off the rails in a way Vibe Check could have prevented? Found a bug? Have an idea for a new feature? That's a great starting point.
12 |
13 | 2. **Fork & clone**: The standard GitHub dance. Fork the repo, clone it locally, and create a branch for your changes.
14 |
15 | ### Local setup
16 |
17 | - Use Node **>=20**.
18 | - Use **npm** for everything: `npm ci`, `npm run build`, `npm test`.
19 |
20 | 3. **Make your changes**: Whether it's code, documentation, or just fixing a typo, all contributions are welcome.
21 |
22 | 4. **Test your changes**: Make sure everything still works as expected.
23 |
24 | 5. **Submit a PR**: Push your changes to your fork and submit a pull request. We'll review it as soon as we can.
25 |
26 | ## Vibe Check Your Contributions
27 |
28 | Before submitting a PR, run your own mental vibe check on your changes:
29 |
30 | - Does this align with the metacognitive purpose of Vibe Check?
31 | - Is this addressing a real problem that AI agents face?
32 | - Does this maintain the balance between developer-friendly vibes and serious AI alignment principles?
33 |
34 | ## What We're Looking For
35 |
36 | ### Code Contributions
37 |
38 | - Bug fixes
39 | - Performance improvements
40 | - New features that align with the project's purpose
41 | - Improvements to the metacognitive questioning system
42 |
43 | ### Documentation Contributions
44 |
45 | - Clarifications to existing documentation
46 | - New examples of how to use Vibe Check effectively
47 | - Case studies of how Vibe Check has helped your agent workflows
48 | - Tutorials for integration with different systems
49 |
50 | ### Pattern Contributions
51 |
52 | - New categories for the `vibe_learn` system
53 | - Common error patterns you've observed in AI agent workflows
54 | - Metacognitive questions that effectively break pattern inertia
55 |
56 | ## Coding Style
57 |
58 | - TypeScript with clear typing
59 | - Descriptive variable names
60 | - Comments that explain the "why," not just the "what"
61 | - Tests for new functionality
62 |
63 | ## The Review Process
64 |
65 | Once you submit a PR, here's what happens:
66 |
67 | 1. A maintainer will review your submission
68 | 2. They might suggest some changes or improvements
69 | 3. Once everything looks good, they'll merge your PR
70 | 4. Your contribution becomes part of Vibe Check!
71 |
72 | ## Share Your Vibe Stories
73 |
74 | We love hearing how people are using Vibe Check in the wild. If you have a story about how Vibe Check saved your agent from a catastrophic reasoning failure or helped simplify an overcomplicated plan, we'd love to hear about it! Submit it as an issue with the tag "vibe story" or mention it in your PR.
75 |
76 | ## Code of Conduct
77 |
78 | - Be respectful and constructive in all interactions
79 | - Focus on the code, not the person
80 | - Help create a welcoming community for all contributors
81 |
82 | ## Questions?
83 |
84 | If you have any questions about contributing, feel free to open an issue with your question. We're here to help!
85 |
86 | Thanks again for considering a contribution to Vibe Check. Together, we can make AI agents a little more self-aware, one pattern interrupt at a time.
```
--------------------------------------------------------------------------------
/Attachments/Template.md:
--------------------------------------------------------------------------------
```markdown
1 | Template
2 |
```
--------------------------------------------------------------------------------
/src/tools/vibeDistil.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Deleted
```
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": "2.7.6"
3 | }
4 |
```
--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://glama.ai/mcp/schemas/server.json",
3 | "maintainers": [
4 | "PV-Bhat"
5 | ]
6 | }
7 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | FROM node:lts-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY . .
6 |
7 | RUN npm install --ignore-scripts
8 | RUN npm run build
9 |
10 | EXPOSE 3000
11 |
12 | CMD ["node", "build/index.js"]
13 |
```
--------------------------------------------------------------------------------
/tests/fixtures/vscode/workspace.mcp.base.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "servers": {
3 | "other": {
4 | "command": "node",
5 | "args": ["tool.js"],
6 | "env": {
7 | "LOG": "debug"
8 | }
9 | }
10 | }
11 | }
12 |
```
--------------------------------------------------------------------------------
/tests/fixtures/cursor/config.base.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "other": {
4 | "command": "python",
5 | "args": ["tool.py"],
6 | "env": {
7 | "TOKEN": "abc"
8 | }
9 | }
10 | }
11 | }
12 |
```
--------------------------------------------------------------------------------
/tests/fixtures/windsurf/config.base.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "existing": {
4 | "command": "npm",
5 | "args": ["run", "tool"],
6 | "env": {
7 | "NODE_ENV": "production"
8 | }
9 | }
10 | }
11 | }
12 |
```
--------------------------------------------------------------------------------
/tests/fixtures/claude/config.base.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "theme": "system",
3 | "mcpServers": {
4 | "other-server": {
5 | "command": "node",
6 | "args": ["other.js"],
7 | "env": {
8 | "TOKEN": "abc123"
9 | }
10 | }
11 | }
12 | }
13 |
```
--------------------------------------------------------------------------------
/tests/fixtures/claude/config.with-other-servers.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "theme": "system",
3 | "mcpServers": {
4 | "vibe-check-mcp": {
5 | "command": "node",
6 | "args": ["legacy.js"],
7 | "env": {}
8 | },
9 | "another": {
10 | "command": "python",
11 | "args": ["server.py"],
12 | "env": {
13 | "DEBUG": "1"
14 | }
15 | }
16 | }
17 | }
18 |
```
--------------------------------------------------------------------------------
/tests/fixtures/windsurf/config.with-http-entry.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "vibe-check-mcp": {
4 | "serverUrl": "http://127.0.0.1:2091",
5 | "managedBy": "vibe-check-mcp-cli"
6 | },
7 | "existing": {
8 | "command": "npm",
9 | "args": ["run", "tool"],
10 | "env": {
11 | "NODE_ENV": "production"
12 | }
13 | }
14 | }
15 | }
16 |
```
--------------------------------------------------------------------------------
/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Docker Image CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 |
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Build the Docker image
18 | run: docker build . --file Dockerfile --tag my-image-name:$(date +%s)
19 |
```
--------------------------------------------------------------------------------
/docs/_toc.md:
--------------------------------------------------------------------------------
```markdown
1 | # Documentation map
2 |
3 | - [Architecture](./architecture.md)
4 | - Integrations
5 | - [CPI Integration](./integrations/cpi.md)
6 | - [Advanced Integration](./advanced-integration.md)
7 | - [Technical Reference](./technical-reference.md)
8 | - [Agent Prompting](./agent-prompting.md)
9 | - [Release & Versioning](./release-workflows.md)
10 |
```
--------------------------------------------------------------------------------
/tests/fixtures/cursor/config.with-managed-entry.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "vibe-check-mcp": {
4 | "command": "npx",
5 | "args": ["@pv-bhat/vibe-check-mcp", "start"],
6 | "env": {},
7 | "managedBy": "vibe-check-mcp-cli"
8 | },
9 | "other": {
10 | "command": "python",
11 | "args": ["tool.py"],
12 | "env": {
13 | "TOKEN": "abc"
14 | }
15 | }
16 | }
17 | }
18 |
```
--------------------------------------------------------------------------------
/src/cli/diff.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createTwoFilesPatch } from 'diff';
2 |
3 | export function formatUnifiedDiff(oldText: string, newText: string, filePath: string): string {
4 | const fromFile = `${filePath} (current)`;
5 | const toFile = `${filePath} (proposed)`;
6 | return createTwoFilesPatch(fromFile, toFile, oldText, newText, '', '', { context: 3 });
7 | }
8 |
```
--------------------------------------------------------------------------------
/request.json:
--------------------------------------------------------------------------------
```json
1 | {"jsonrpc":"2.0","method":"tools/call","params":{"name":"vibe_check","arguments":{"goal":"Test session history functionality","plan":"2. Make a second call to verify history is included.","userPrompt":"Please test the history feature.","progress":"Just made the second call.","sessionId":"history-test-session-1"}},"id":2}
```
--------------------------------------------------------------------------------
/tests/fixtures/claude/config.with-managed-entry.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "theme": "system",
3 | "mcpServers": {
4 | "vibe-check-mcp": {
5 | "command": "npx",
6 | "args": ["@pv-bhat/vibe-check-mcp", "start"],
7 | "env": {},
8 | "managedBy": "vibe-check-mcp-cli"
9 | },
10 | "other": {
11 | "command": "ruby",
12 | "args": ["tool.rb"],
13 | "env": {
14 | "LEVEL": "info"
15 | }
16 | }
17 | }
18 | }
19 |
```
--------------------------------------------------------------------------------
/tests/fixtures/windsurf/config.with-managed-entry.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "vibe-check-mcp": {
4 | "command": "npx",
5 | "args": ["-y", "@pv-bhat/vibe-check-mcp", "start", "--stdio"],
6 | "env": {},
7 | "managedBy": "vibe-check-mcp-cli"
8 | },
9 | "existing": {
10 | "command": "npm",
11 | "args": ["run", "tool"],
12 | "env": {
13 | "NODE_ENV": "production"
14 | }
15 | }
16 | }
17 | }
18 |
```
--------------------------------------------------------------------------------
/tests/fixtures/vscode/workspace.mcp.with-managed.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "servers": {
3 | "vibe-check-mcp": {
4 | "command": "npx",
5 | "args": ["-y", "@pv-bhat/vibe-check-mcp", "start", "--stdio"],
6 | "env": {},
7 | "transport": "stdio",
8 | "managedBy": "vibe-check-mcp-cli"
9 | },
10 | "other": {
11 | "command": "node",
12 | "args": ["tool.js"],
13 | "env": {
14 | "LOG": "debug"
15 | }
16 | }
17 | }
18 | }
19 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "esModuleInterop": true,
7 | "outDir": "build",
8 | "strict": true,
9 | "declaration": false,
10 | "sourceMap": false,
11 | "types": [
12 | "node",
13 | "vitest/globals"
14 | ]
15 | },
16 | "include": [
17 | "src/**/*",
18 | "tests/**/*"
19 | ],
20 | "exclude": [
21 | "node_modules",
22 | "**/*.test.ts"
23 | ]
24 | }
25 |
```
--------------------------------------------------------------------------------
/alt-test-gemini.js:
--------------------------------------------------------------------------------
```javascript
1 |
2 | import fs from 'fs';
3 |
4 | const request = JSON.stringify({
5 | jsonrpc: '2.0',
6 | method: 'tools/call',
7 | params: {
8 | name: 'vibe_check',
9 | arguments: {
10 | goal: 'Test default Gemini provider',
11 | plan: '2. Make a call to vibe_check using the default Gemini provider.',
12 | }
13 | },
14 | id: 2
15 | });
16 |
17 | fs.writeFileSync('request.json', request, 'utf-8');
18 |
19 | console.log('Generated request.json for the Gemini test.');
20 |
```
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineConfig } from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | environment: 'node',
6 | globals: true,
7 | include: ['tests/**/*.test.ts'],
8 | coverage: {
9 | provider: 'v8',
10 | reporter: ['text', 'html', 'json-summary'],
11 | all: true,
12 | include: ['src/**/*.ts'],
13 | exclude: ['**/alt-test*.js', 'test-client.*', 'src/tools/vibeDistil.ts', 'src/tools/vibeLearn.ts'],
14 | thresholds: { lines: 80 }
15 | }
16 | }
17 | });
18 |
```
--------------------------------------------------------------------------------
/src/utils/version.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createRequire } from 'module';
2 |
3 | const require = createRequire(import.meta.url);
4 |
5 | let cachedVersion: string | null = null;
6 |
7 | export function getPackageVersion(): string {
8 | if (cachedVersion) {
9 | return cachedVersion;
10 | }
11 |
12 | const pkg = require('../../package.json') as { version?: string };
13 | const version = pkg?.version;
14 |
15 | if (!version) {
16 | throw new Error('Package version is missing from package.json');
17 | }
18 |
19 | cachedVersion = version;
20 | return cachedVersion;
21 | }
22 |
```
--------------------------------------------------------------------------------
/test.json:
--------------------------------------------------------------------------------
```json
1 | {"id":"1","jsonrpc":"2.0","method":"tools/call","params":{"name":"vibe_check","arguments":{"goal":"Implement the core logic for the new feature","plan":"1. Define the data structures. 2. Implement the main algorithm. 3. Add error handling.","userPrompt":"Create a new feature that does X, Y, and Z.","progress":"Just started","uncertainties":["The third-party API might be unreliable"],"taskContext":"This is part of a larger project to refactor the billing module.","sessionId":"test-session-123"}}}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | ## Unreleased
4 |
5 | - Introduce CLI scaffold and npx bin (no commands yet).
6 |
7 | ## v2.7.1 - 2025-10-11
8 |
9 | - Added `install --client cursor|windsurf|vscode` adapters with managed-entry merges, atomic writes, and `.bak` rollbacks.
10 | - Preserved Windsurf `serverUrl` HTTP entries and emitted VS Code workspace snippets plus `vscode:mcp/install` links when configs are absent.
11 | - Updated documentation with consolidated provider-key guidance, transport selection, uninstall tips, and a dedicated [clients guide](docs/clients.md).
12 |
```
--------------------------------------------------------------------------------
/alt-test-openai.js:
--------------------------------------------------------------------------------
```javascript
1 |
2 | import fs from 'fs';
3 |
4 | const request = JSON.stringify({
5 | jsonrpc: '2.0',
6 | method: 'tools/call',
7 | params: {
8 | name: 'vibe_check',
9 | arguments: {
10 | goal: 'Test OpenAI provider',
11 | plan: '1. Make a call to vibe_check using the OpenAI provider.',
12 | modelOverride: {
13 | provider: 'openai',
14 | model: 'o4-mini'
15 | }
16 | }
17 | },
18 | id: 1
19 | });
20 |
21 | fs.writeFileSync('request.json', request, 'utf-8');
22 |
23 | console.log('Generated request.json for the OpenAI test.');
24 |
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | fast-checks:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 20
17 | - run: npm ci
18 | - run: npm run build
19 | - run: npm run test
20 | - run: node build/cli/index.js doctor
21 | - run: node build/cli/index.js start --stdio --dry-run
22 | - run: node build/cli/index.js start --http --port 2091 --dry-run
23 | - name: Security Scan
24 | run: npm run security-check
25 |
```
--------------------------------------------------------------------------------
/docs/registry-descriptions.md:
--------------------------------------------------------------------------------
```markdown
1 | # Registry Descriptions
2 |
3 | These short descriptions can be used when submitting VibeCheck MCP to external registries or directories.
4 |
5 | ## Smithery.ai
6 | ```
7 | Metacognitive oversight MCP server for AI agents – adaptive CPI interrupts for alignment and safety.
8 | ```
9 |
10 | ## Glama Directory
11 | ```
12 | Metacognitive layer for Llama-compatible agents via MCP. Enhances reflection, accountability and robustness.
13 | ```
14 |
15 | ## Awesome MCP Lists PR Draft
16 | ```
17 | - [VibeCheck MCP](https://github.com/PV-Bhat/vibe-check-mcp-server) - Adaptive sanity check server preventing cascading errors in AI agents.
18 | ```
19 |
```
--------------------------------------------------------------------------------
/test-client.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3 | import { spawn } from 'child_process';
4 |
5 | async function testVibeCheck() {
6 | const serverProcess = spawn('node', ['build/index.js'], { stdio: ['pipe', 'pipe', 'pipe'] });
7 |
8 | await new Promise(resolve => setTimeout(resolve, 1000));
9 |
10 | const transport = new StdioClientTransport(serverProcess);
11 | const client = new Client(transport);
12 |
13 | const response = await client.tool('vibe_check', { goal: 'Test goal', plan: 'Test plan', progress: 'Initial stage' });
14 |
15 | console.log('Response:', response);
16 |
17 | await transport.close();
18 | serverProcess.kill();
19 | }
20 |
21 | testVibeCheck();
```
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | ## v2.5.0 — 2025-09-03
4 | - Transport: migrate STDIO → Streamable HTTP (`POST /mcp`, `GET /mcp` → 405).
5 | - Constitution tools: `update_constitution`, `reset_constitution`, `check_constitution` (session-scoped, in-memory, logged).
6 | - CPI surfaced: banner + concise metrics; links to ResearchGate, CPI GitHub, and Zenodo (MURST).
7 |
8 | ## v2.2.0 - 2025-07-22
9 | - CPI architecture enables adaptive interrupts to mitigate Reasoning Lock-In
10 | - History continuity across sessions
11 | - Multi-provider support for Gemini, OpenAI and OpenRouter
12 | - Optional vibe_learn logging for privacy-conscious deployments
13 | - Repository restructured with Vitest unit tests and CI workflow
14 |
15 | ## v1.1.0 - 2024-06-10
16 | - Initial feedback loop and Docker setup
17 |
```
--------------------------------------------------------------------------------
/tests/constitution.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from 'vitest';
2 | import { updateConstitution, resetConstitution, getConstitution, __testing } from '../src/tools/constitution.js';
3 |
4 | describe('constitution utilities', () => {
5 | it('updates, resets, and retrieves rules', () => {
6 | updateConstitution('s1', 'r1');
7 | updateConstitution('s1', 'r2');
8 | expect(getConstitution('s1')).toEqual(['r1', 'r2']);
9 |
10 | resetConstitution('s1', ['a']);
11 | expect(getConstitution('s1')).toEqual(['a']);
12 | });
13 |
14 | it('cleans up stale sessions', () => {
15 | updateConstitution('s2', 'rule');
16 | const map = __testing._getMap();
17 | map['s2'].updated = Date.now() - 2 * 60 * 60 * 1000;
18 | __testing.cleanup();
19 | expect(getConstitution('s2')).toEqual([]);
20 | });
21 | });
22 |
```
--------------------------------------------------------------------------------
/server.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
3 | "name": "io.github.PV-Bhat/vibe-check-mcp-server",
4 | "description": "Metacognitive AI agent oversight: adaptive CPI interrupts for alignment, reflection and safety",
5 | "status": "active",
6 | "repository": {
7 | "url": "https://github.com/PV-Bhat/vibe-check-mcp-server",
8 | "source": "github"
9 | },
10 | "version": "1.0.0",
11 | "packages": [
12 | {
13 | "registry_type": "npm",
14 | "identifier": "@pv-bhat/vibe-check-mcp",
15 | "version": "2.5.1",
16 | "transport": {
17 | "type": "stdio"
18 | },
19 | "environment_variables": [
20 | {
21 | "description": "Your API key for the service",
22 | "is_required": true,
23 | "format": "string",
24 | "is_secret": true,
25 | "name": "YOUR_API_KEY"
26 | }
27 | ]
28 | }
29 | ]
30 | }
```
--------------------------------------------------------------------------------
/tests/cli-version.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from 'vitest';
2 | import { execFileSync } from 'node:child_process';
3 | import { existsSync, readFileSync } from 'node:fs';
4 | import { dirname, resolve } from 'node:path';
5 | import { fileURLToPath } from 'node:url';
6 |
7 | describe('CLI version', () => {
8 | it('prints the package version', () => {
9 | const __filename = fileURLToPath(import.meta.url);
10 | const __dirname = dirname(__filename);
11 | const projectRoot = resolve(__dirname, '..');
12 | const cliPath = resolve(projectRoot, 'build', 'cli', 'index.js');
13 | expect(existsSync(cliPath)).toBe(true);
14 |
15 | const output = execFileSync('node', [cliPath, '--version'], { encoding: 'utf8' }).trim();
16 | const pkg = JSON.parse(readFileSync(resolve(projectRoot, 'package.json'), 'utf8')) as { version: string };
17 |
18 | expect(output).toBe(pkg.version);
19 | });
20 | });
21 |
```
--------------------------------------------------------------------------------
/tests/index-main.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { afterEach, describe, expect, it, vi } from 'vitest';
2 |
3 | describe('main entrypoint', () => {
4 | afterEach(() => {
5 | vi.restoreAllMocks();
6 | });
7 |
8 | it('initializes the HTTP server when stdio transport is disabled', async () => {
9 | vi.resetModules();
10 | const module = await import('../src/index.js');
11 | const serverMock = { connect: vi.fn() } as unknown as import('@modelcontextprotocol/sdk/server/index.js').Server;
12 | const startMock = vi
13 | .fn<Parameters<typeof module.startHttpServer>, ReturnType<typeof module.startHttpServer>>()
14 | .mockResolvedValue({ app: {} as any, listener: { close: vi.fn() } as any, transport: {} as any, close: vi.fn() });
15 |
16 | await module.main({
17 | createServer: async () => serverMock,
18 | startHttp: startMock,
19 | });
20 |
21 | expect(startMock).toHaveBeenCalledWith(expect.objectContaining({ server: serverMock, attachSignalHandlers: true, logger: console }));
22 | });
23 | });
24 |
```
--------------------------------------------------------------------------------
/src/utils/httpTransportWrapper.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AsyncLocalStorage } from 'node:async_hooks';
2 |
3 | import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4 |
5 | export interface RequestScopeStore {
6 | forceJson?: boolean;
7 | }
8 |
9 | const WRAPPED_SYMBOL: unique symbol = Symbol.for('vibe-check.requestScopedTransport');
10 |
11 | export function createRequestScopedTransport(
12 | transport: StreamableHTTPServerTransport,
13 | scope: AsyncLocalStorage<RequestScopeStore>
14 | ): StreamableHTTPServerTransport {
15 | const existing = (transport as any)[WRAPPED_SYMBOL];
16 | if (existing) {
17 | return transport;
18 | }
19 |
20 | let storedValue = (transport as any)._enableJsonResponse ?? false;
21 |
22 | Object.defineProperty(transport, '_enableJsonResponse', {
23 | configurable: true,
24 | enumerable: false,
25 | get() {
26 | const store = scope.getStore();
27 | if (store?.forceJson) {
28 | return true;
29 | }
30 | return storedValue;
31 | },
32 | set(value: boolean) {
33 | storedValue = value;
34 | }
35 | });
36 |
37 | (transport as any)[WRAPPED_SYMBOL] = true;
38 | return transport;
39 | }
40 |
```
--------------------------------------------------------------------------------
/test-client.js:
--------------------------------------------------------------------------------
```javascript
1 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3 |
4 | async function main() {
5 | const transport = new StdioClientTransport({ command: 'node', args: ['build/index.js'] });
6 | const client = new Client({ transport });
7 |
8 | const request = {
9 | name: 'vibe_check',
10 | arguments: {
11 | goal: 'Implement the core logic for the new feature',
12 | plan: '1. Define the data structures. 2. Implement the main algorithm. 3. Add error handling.',
13 | userPrompt: 'Create a new feature that does X, Y, and Z.',
14 | progress: 'Just started',
15 | uncertainties: ['The third-party API might be unreliable'],
16 | taskContext: 'This is part of a larger project to refactor the billing module.',
17 | sessionId: 'test-session-123',
18 | },
19 | };
20 |
21 | try {
22 | await client.connect();
23 | const response = await client.callTool(request.name, request.arguments);
24 | console.log(JSON.stringify(response, null, 2));
25 | } catch (error) {
26 | console.error(error);
27 | } finally {
28 | transport.destroy();
29 | }
30 | }
31 |
32 | main();
```
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { spawn } from 'child_process';const server = spawn('node', ['build/index.js']);const request = { id: '1', jsonrpc: '2.0', method: 'tools/call', params: { name: 'vibe_check', arguments: { goal: 'Implement the core logic for the new feature', plan: '1. Define the data structures. 2. Implement the main algorithm. 3. Add error handling.', userPrompt: 'Create a new feature that does X, Y, and Z.', progress: 'Just started', uncertainties: ['The third-party API might be unreliable'], taskContext: 'This is part of a larger project to refactor the billing module.', sessionId: 'test-session-123', }, },};const message = JSON.stringify(request);const length = Buffer.byteLength(message, 'utf-8');const header = `Content-Length: ${length}\r\n\r\n`;server.stdout.on('data', (data) => { console.log(`${data}`);});server.stderr.on('data', (data) => { console.error(`stderr: ${data}`);});server.on('close', (code) => { console.log(`child process exited with code ${code}`);});server.stdin.write(header);server.stdin.write(message);server.stdin.end();
```
--------------------------------------------------------------------------------
/tests/state.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
2 | import * as fs from 'fs/promises';
3 | import { loadHistory, getHistorySummary, addToHistory } from '../src/utils/state.js';
4 |
5 | vi.mock('fs/promises');
6 | const mockedFs = fs as unknown as { readFile: ReturnType<typeof vi.fn>; writeFile: ReturnType<typeof vi.fn>; mkdir: ReturnType<typeof vi.fn>; };
7 |
8 | beforeEach(async () => {
9 | vi.clearAllMocks();
10 | mockedFs.mkdir = vi.fn();
11 | mockedFs.readFile = vi.fn().mockResolvedValue('{}');
12 | mockedFs.writeFile = vi.fn();
13 | await loadHistory();
14 | });
15 |
16 | describe('state history', () => {
17 | it('initializes empty history if none', async () => {
18 | mockedFs.readFile.mockRejectedValue(new Error('missing'));
19 | await loadHistory();
20 | expect(getHistorySummary('none')).toBe('');
21 | });
22 |
23 | it('adds to history and trims to 10', async () => {
24 | mockedFs.readFile.mockRejectedValue(new Error('missing'));
25 | await loadHistory();
26 | for (let i = 1; i <= 11; i++) {
27 | addToHistory('sess', { goal: `g${i}`, plan: `p${i}` }, `o${i}`);
28 | }
29 | await Promise.resolve();
30 | const summary = getHistorySummary('sess');
31 | expect(summary).toContain('g7');
32 | expect(summary).not.toContain('g2');
33 | });
34 | });
35 |
```
--------------------------------------------------------------------------------
/tests/vibeLearn.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
2 | import { vibeLearnTool } from '../src/tools/vibeLearn.js';
3 | import * as storage from '../src/utils/storage.js';
4 |
5 | vi.mock('../src/utils/storage.js');
6 |
7 | const mockedStorage = storage as unknown as {
8 | addLearningEntry: ReturnType<typeof vi.fn>;
9 | getLearningCategorySummary: ReturnType<typeof vi.fn>;
10 | getLearningEntries: ReturnType<typeof vi.fn>;
11 | };
12 |
13 | beforeEach(() => {
14 | vi.clearAllMocks();
15 | mockedStorage.addLearningEntry = vi.fn(() => ({
16 | type: 'mistake',
17 | category: 'Test',
18 | mistake: 'm',
19 | solution: 's',
20 | timestamp: Date.now()
21 | }));
22 | mockedStorage.getLearningEntries = vi.fn(() => ({ Test: [] }));
23 | mockedStorage.getLearningCategorySummary = vi.fn(() => [{ category: 'Test', count: 1, recentExample: { mistake: 'm', solution: 's', type: 'mistake', timestamp: Date.now() } }]);
24 | });
25 |
26 | describe('vibeLearnTool', () => {
27 | it('logs entry and returns summary', async () => {
28 | const res = await vibeLearnTool({ mistake: 'm', category: 'Test', solution: 's' });
29 | expect(res.added).toBe(true);
30 | expect(mockedStorage.addLearningEntry).toHaveBeenCalled();
31 | expect(res.topCategories[0].category).toBe('Test');
32 | });
33 | });
34 |
```
--------------------------------------------------------------------------------
/docs/docker-automation.md:
--------------------------------------------------------------------------------
```markdown
1 | # Automatic Docker Setup
2 |
3 | This guide shows how to run the Vibe Check MCP server in Docker and configure it to start automatically with Cursor.
4 |
5 | ## Prerequisites
6 |
7 | - Docker and Docker Compose installed and available in your `PATH`.
8 | - A Gemini API key for the server.
9 |
10 | ## Quick Start
11 |
12 | Run the provided setup script from the repository root:
13 |
14 | ```bash
15 | bash scripts/docker-setup.sh
16 | ```
17 |
18 | The script performs the following actions:
19 |
20 | 1. Creates `~/vibe-check-mcp` and copies required files.
21 | 2. Builds the Docker image and sets up `docker-compose.yml`.
22 | 3. Prompts for your `GEMINI_API_KEY` and stores it in `~/vibe-check-mcp/.env`.
23 | 4. Configures a systemd service on Linux or a LaunchAgent on macOS so the container starts on login.
24 | 5. Generates `vibe-check-tcp-wrapper.sh` which proxies STDIO to the container on port 3000.
25 | 6. Starts the container in the background.
26 |
27 | After running the script, configure Cursor IDE:
28 |
29 | 1. Open **Settings** → **MCP**.
30 | 2. Choose **Add New MCP Server**.
31 | 3. Set the type to **Command** and use the wrapper script path:
32 | `~/vibe-check-mcp/vibe-check-tcp-wrapper.sh`.
33 | 4. Save and refresh.
34 |
35 | Vibe Check MCP will now launch automatically whenever you log in and be available to Cursor without additional manual steps.
36 |
```
--------------------------------------------------------------------------------
/src/utils/anthropic.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface AnthropicConfig {
2 | baseUrl: string;
3 | apiKey?: string;
4 | authToken?: string;
5 | version: string;
6 | }
7 |
8 | export interface AnthropicHeaderInput {
9 | apiKey?: string;
10 | authToken?: string;
11 | version: string;
12 | }
13 |
14 | export function resolveAnthropicConfig(): AnthropicConfig {
15 | const trimmedBase = process.env.ANTHROPIC_BASE_URL?.replace(/\/+$/, '');
16 | const baseUrl = trimmedBase && trimmedBase.length > 0 ? trimmedBase : 'https://api.anthropic.com';
17 | const apiKey = process.env.ANTHROPIC_API_KEY;
18 | const authToken = process.env.ANTHROPIC_AUTH_TOKEN;
19 | const version = process.env.ANTHROPIC_VERSION || '2023-06-01';
20 |
21 | if (!apiKey && !authToken) {
22 | throw new Error(
23 | 'Anthropic configuration error: set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN when provider "anthropic" is selected.'
24 | );
25 | }
26 |
27 | return {
28 | baseUrl,
29 | apiKey,
30 | authToken,
31 | version,
32 | };
33 | }
34 |
35 | export function buildAnthropicHeaders({ apiKey, authToken, version }: AnthropicHeaderInput): Record<string, string> {
36 | const headers: Record<string, string> = {
37 | 'content-type': 'application/json',
38 | 'anthropic-version': version,
39 | };
40 |
41 | if (apiKey) {
42 | headers['x-api-key'] = apiKey;
43 | } else if (authToken) {
44 | headers.authorization = `Bearer ${authToken}`;
45 | }
46 |
47 | return headers;
48 | }
49 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | # Metadata for discoverability and registry listing
4 | name: vibe-check-mcp
5 | version: 2.5.0
6 | description: Metacognitive AI agent oversight tool implementing CPI-driven interrupts for alignment and safety.
7 | author: PV-Bhat
8 | repository: https://github.com/PV-Bhat/vibe-check-mcp-server
9 | license: MIT
10 | category: ai-tools
11 | tags:
12 | - cpi chain pattern interrupts
13 | - pruthvi bhat
14 | - rli reasoning lock in
15 | - murst
16 | - metacognition
17 | - workflow-optimization
18 | - gemini
19 | - openai
20 | - openrouter
21 | capabilities:
22 | - meta-mentorship
23 | - agentic oversight
24 | - chain pattern-interrupt
25 | - vibe-check
26 | - self-improving-feedback
27 | - multi-provider-llm
28 |
29 | # Requirements (e.g., for local setup)
30 | requirements:
31 | node: ">=18.0.0"
32 |
33 | # Installation options
34 | installation:
35 | npm: "@mseep/vibe-check-mcp" # For manual npm install
36 |
37 | startCommand:
38 | type: http
39 | command: node build/index.js
40 | env:
41 | MCP_HTTP_PORT: "3000"
42 | MCP_DISCOVERY_MODE: "1"
43 |
44 | http:
45 | endpoint: "/mcp"
46 | cors:
47 | origin: "${CORS_ORIGIN:-*}"
48 |
49 | # Documentation links
50 | documentation:
51 | getting_started: https://github.com/PV-Bhat/vibe-check-mcp-server#installation
52 | configuration: https://github.com/PV-Bhat/vibe-check-mcp-server#configuration
53 | technical_reference: https://github.com/PV-Bhat/vibe-check-mcp-server/blob/main/docs/technical-reference.md
54 |
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 | workflow_dispatch:
8 |
9 | jobs:
10 | publish-npm:
11 | runs-on: ubuntu-latest
12 | env:
13 | GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: actions/setup-node@v4
17 | with:
18 | node-version: 20
19 | registry-url: 'https://registry.npmjs.org'
20 | - run: npm ci
21 | - run: npm run build
22 | - run: npm run test:coverage
23 | - run: node build/cli/index.js doctor
24 | - run: node build/cli/index.js start --stdio --dry-run
25 | - run: node build/cli/index.js start --http --port 2091 --dry-run
26 | - name: Verify package contents
27 | run: |
28 | PKG_TGZ=$(npm pack --silent)
29 | tar -tf "$PKG_TGZ" | grep -q 'package/build/cli/index.js'
30 | node -e "const p=require('./package.json');process.exit((p.bin&&Object.values(p.bin).includes('build/cli/index.js'))?0:1)"
31 | - name: Publish to npmjs
32 | env:
33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
34 | run: npm publish --access public
35 | - name: Post-publish smoke via npx
36 | run: |
37 | PKG=$(node -p "require('./package.json').name")
38 | VER=$(node -p "require('./package.json').version")
39 | npx -y "$PKG@$VER" --help
40 | npx -y "$PKG@$VER" start --stdio --dry-run
41 | npx -y "$PKG@$VER" start --http --port 2091 --dry-run
42 |
```
--------------------------------------------------------------------------------
/src/tools/constitution.ts:
--------------------------------------------------------------------------------
```typescript
1 | interface ConstitutionEntry {
2 | rules: string[];
3 | updated: number;
4 | }
5 |
6 | const constitutionMap: Record<string, ConstitutionEntry> = Object.create(null);
7 |
8 | const MAX_RULES_PER_SESSION = 50;
9 | const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
10 |
11 | export function updateConstitution(sessionId: string, rule: string) {
12 | if (!sessionId || !rule) return;
13 | const entry = constitutionMap[sessionId] || { rules: [], updated: 0 };
14 | if (entry.rules.length >= MAX_RULES_PER_SESSION) entry.rules.shift();
15 | entry.rules.push(rule);
16 | entry.updated = Date.now();
17 | constitutionMap[sessionId] = entry;
18 | }
19 |
20 | export function resetConstitution(sessionId: string, rules: string[]) {
21 | if (!sessionId || !Array.isArray(rules)) return;
22 | constitutionMap[sessionId] = {
23 | rules: rules.slice(0, MAX_RULES_PER_SESSION),
24 | updated: Date.now()
25 | };
26 | }
27 |
28 | export function getConstitution(sessionId: string): string[] {
29 | const entry = constitutionMap[sessionId];
30 | if (!entry) return [];
31 | entry.updated = Date.now();
32 | return entry.rules;
33 | }
34 |
35 | // Cleanup stale sessions to prevent unbounded memory growth
36 | function cleanup() {
37 | const now = Date.now();
38 | for (const [sessionId, entry] of Object.entries(constitutionMap)) {
39 | if (now - entry.updated > SESSION_TTL_MS) {
40 | delete constitutionMap[sessionId];
41 | }
42 | }
43 | }
44 |
45 | setInterval(cleanup, SESSION_TTL_MS).unref();
46 |
47 | export const __testing = {
48 | _getMap: () => constitutionMap,
49 | cleanup
50 | };
51 |
```
--------------------------------------------------------------------------------
/tests/cli-install-vscode-dry-run.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
2 | import { createCliProgram } from '../src/cli/index.js';
3 |
4 | const ORIGINAL_ENV = { ...process.env };
5 |
6 | describe('cli install vscode manual guidance', () => {
7 | beforeEach(() => {
8 | process.exitCode = undefined;
9 | process.env = { ...ORIGINAL_ENV };
10 | });
11 |
12 | afterEach(() => {
13 | vi.restoreAllMocks();
14 | process.env = { ...ORIGINAL_ENV };
15 | });
16 |
17 | it('prints manual instructions and install link when config is missing', async () => {
18 | process.env.OPENAI_API_KEY = 'sk-manual-test';
19 |
20 | const logs: string[] = [];
21 | const logSpy = vi.spyOn(console, 'log').mockImplementation((message?: unknown) => {
22 | logs.push(String(message ?? ''));
23 | });
24 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
25 |
26 | const program = createCliProgram();
27 | await program.parseAsync([
28 | 'node',
29 | 'vibe-check-mcp',
30 | 'install',
31 | '--client',
32 | 'vscode',
33 | '--http',
34 | '--port',
35 | '3001',
36 | '--dry-run',
37 | '--non-interactive',
38 | ]);
39 |
40 | logSpy.mockRestore();
41 | warnSpy.mockRestore();
42 |
43 | const output = logs.join('\n');
44 | expect(output).toContain('configuration not found');
45 | expect(output).toContain('Add this MCP server configuration manually');
46 | expect(output).toContain('VS Code quick install link');
47 | expect(output).toContain('vscode:mcp/install');
48 | expect(output).toContain('http://127.0.0.1:3001');
49 | expect(output).toContain('--http');
50 | });
51 | });
52 |
```
--------------------------------------------------------------------------------
/src/cli/clients/cursor.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { join } from 'node:path';
2 | import os from 'node:os';
3 | import {
4 | ClientAdapter,
5 | JsonRecord,
6 | MergeOpts,
7 | MergeResult,
8 | expandHomePath,
9 | mergeIntoMap,
10 | pathExists,
11 | readJsonFile,
12 | writeJsonFileAtomic,
13 | } from './shared.js';
14 |
15 | const locateCursorConfig = async (customPath?: string): Promise<string | null> => {
16 | if (customPath) {
17 | return expandHomePath(customPath);
18 | }
19 |
20 | const home = os.homedir();
21 | const candidate = join(home, '.cursor', 'mcp.json');
22 | if (await pathExists(candidate)) {
23 | return candidate;
24 | }
25 |
26 | return null;
27 | };
28 |
29 | const readCursorConfig = async (path: string, raw?: string): Promise<JsonRecord> => {
30 | return readJsonFile(path, raw);
31 | };
32 |
33 | const mergeCursorEntry = (config: JsonRecord, entry: JsonRecord, options: MergeOpts): MergeResult => {
34 | return mergeIntoMap(config, entry, options, 'mcpServers');
35 | };
36 |
37 | const writeCursorConfigAtomic = async (path: string, data: JsonRecord): Promise<void> => {
38 | await writeJsonFileAtomic(path, data);
39 | };
40 |
41 | const adapter: ClientAdapter = {
42 | locate: locateCursorConfig,
43 | read: readCursorConfig,
44 | merge: mergeCursorEntry,
45 | writeAtomic: writeCursorConfigAtomic,
46 | describe() {
47 | return {
48 | name: 'Cursor',
49 | pathHint: '~/.cursor/mcp.json',
50 | summary: 'Cursor IDE with Claude-style MCP configuration.',
51 | transports: ['stdio'],
52 | defaultTransport: 'stdio',
53 | notes: 'Open Cursor Settings → MCP Servers if the file does not exist yet.',
54 | docsUrl: 'https://docs.cursor.com/ai/model-context-protocol',
55 | };
56 | },
57 | };
58 |
59 | export default adapter;
60 |
```
--------------------------------------------------------------------------------
/alt-test.js:
--------------------------------------------------------------------------------
```javascript
1 | import fs from 'fs';
2 |
3 | function createVibeCheckRequest(id, goal, plan, userPrompt, progress, sessionId) {
4 | return JSON.stringify({
5 | jsonrpc: '2.0',
6 | method: 'tools/call',
7 | params: {
8 | name: 'vibe_check',
9 | arguments: {
10 | goal: goal,
11 | plan: plan,
12 | userPrompt: userPrompt,
13 | progress: progress,
14 | sessionId: sessionId,
15 | modelOverride: {
16 | provider: 'openrouter',
17 | model: 'tngtech/deepseek-r1t2-chimera:free'
18 | }
19 | }
20 | },
21 | id: id
22 | });
23 | }
24 |
25 | const sessionId = 'history-test-session-phase4';
26 |
27 | // First call
28 | const request1 = createVibeCheckRequest(
29 | 1,
30 | 'Test new meta-mentor prompt and history functionality',
31 | '1. Make the first call to establish history.',
32 | 'Please test the new meta-mentor prompt and history feature.',
33 | 'Starting the test.',
34 | sessionId
35 | );
36 | fs.writeFileSync('request1.json', request1, 'utf-8');
37 | console.log('Generated request1.json for the first call.');
38 |
39 | // Second call
40 | const request2 = createVibeCheckRequest(
41 | 2,
42 | 'Test new meta-mentor prompt and history functionality',
43 | '2. Make the second call to verify history is included and prompt tone.',
44 | 'Please test the new meta-mentor prompt and history feature.',
45 | 'Just made the second call, expecting history context.',
46 | sessionId
47 | );
48 | fs.writeFileSync('request2.json', request2, 'utf-8');
49 | console.log('Generated request2.json for the second call.');
```
--------------------------------------------------------------------------------
/docs/gemini.md:
--------------------------------------------------------------------------------
```markdown
1 | # Agent Quickstart
2 |
3 | Vibe Check MCP is a lightweight oversight layer for AI agents. It exposes two tools:
4 |
5 | - **vibe_check** – prompts you with clarifying questions to prevent tunnel vision.
6 | - **vibe_learn** – optional logging of mistakes and successes for later review.
7 |
8 | The server supports Gemini, OpenAI and OpenRouter LLMs. History is maintained across requests when a `sessionId` is provided.
9 |
10 | ## Setup
11 |
12 | 1. Install dependencies and build:
13 | ```bash
14 | npm install
15 | npm run build
16 | ```
17 | 2. Supply the following environment variables as needed:
18 | - `GEMINI_API_KEY`
19 | - `OPENAI_API_KEY`
20 | - `OPENROUTER_API_KEY`
21 | - `DEFAULT_LLM_PROVIDER` (gemini | openai | openrouter)
22 | - `DEFAULT_MODEL` (e.g., gemini-2.5-pro)
23 | 3. Start the server:
24 | ```bash
25 | npm start
26 | ```
27 |
28 | ## Testing
29 |
30 | Run unit tests with `npm test`. The built-in JSON-RPC compatibility layer mitigates missing `id` fields on `tools/call` requests so the stock SDK client and Windsurf can connect without any special tooling, but well-behaved clients should still supply their own identifiers. Example request generators are still provided if you prefer ready-made payloads for manual testing:
31 |
32 | - `alt-test-gemini.js`
33 | - `alt-test-openai.js`
34 | - `alt-test.js` (OpenRouter)
35 |
36 | Each script writes a `request.json` file that you can pipe to the server:
37 |
38 | ```bash
39 | node build/index.js < request.json
40 | ```
41 |
42 | ## Integration Tips
43 |
44 | Call `vibe_check` regularly with your goal, plan and current progress. Use `vibe_learn` whenever you want to record a resolved issue. Full API details are in `docs/technical-reference.md`.
45 |
```
--------------------------------------------------------------------------------
/src/cli/doctor.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { existsSync } from 'node:fs';
2 | import { readFileSync } from 'node:fs';
3 | import { resolve } from 'node:path';
4 | import net from 'node:net';
5 | import { parse as parseEnv } from 'dotenv';
6 | import semver from 'semver';
7 | import { homeConfigDir } from './env.js';
8 |
9 | export function checkNodeVersion(requiredRange: string, currentVersion: string = process.version): {
10 | ok: boolean;
11 | current: string;
12 | } {
13 | const current = currentVersion;
14 | const coerced = semver.coerce(current);
15 | const satisfies = coerced ? semver.satisfies(coerced, requiredRange) : false;
16 | return {
17 | ok: satisfies,
18 | current,
19 | };
20 | }
21 |
22 | export async function portStatus(port: number): Promise<'free' | 'in-use' | 'unknown'> {
23 | return new Promise((resolveStatus) => {
24 | const tester = net.createServer();
25 |
26 | tester.once('error', (error: NodeJS.ErrnoException) => {
27 | if (error.code === 'EADDRINUSE') {
28 | resolveStatus('in-use');
29 | } else {
30 | resolveStatus('unknown');
31 | }
32 | });
33 |
34 | tester.once('listening', () => {
35 | tester.close(() => resolveStatus('free'));
36 | });
37 |
38 | try {
39 | tester.listen({ port, host: '127.0.0.1' });
40 | } catch {
41 | resolveStatus('unknown');
42 | }
43 | });
44 | }
45 |
46 | export function detectEnvFiles(): {
47 | cwdEnv: string | null;
48 | homeEnv: string | null;
49 | } {
50 | const cwdEnvPath = resolve(process.cwd(), '.env');
51 | const homeEnvPath = resolve(homeConfigDir(), '.env');
52 |
53 | return {
54 | cwdEnv: existsSync(cwdEnvPath) ? cwdEnvPath : null,
55 | homeEnv: existsSync(homeEnvPath) ? homeEnvPath : null,
56 | };
57 | }
58 |
59 | export function readEnvFile(path: string): Record<string, string> {
60 | const raw = readFileSync(path, 'utf8');
61 | return parseEnv(raw);
62 | }
63 |
```
--------------------------------------------------------------------------------
/src/utils/state.ts:
--------------------------------------------------------------------------------
```typescript
1 |
2 |
3 | import fs from 'fs/promises';
4 | import path from 'path';
5 | import os from 'os';
6 | import { VibeCheckInput } from '../tools/vibeCheck.js';
7 |
8 | const DATA_DIR = path.join(os.homedir(), '.vibe-check');
9 | const HISTORY_FILE = path.join(DATA_DIR, 'history.json');
10 |
11 | interface Interaction {
12 | input: VibeCheckInput;
13 | output: string;
14 | timestamp: number;
15 | }
16 |
17 | let history: Map<string, Interaction[]> = new Map();
18 |
19 | async function ensureDataDir() {
20 | try {
21 | await fs.mkdir(DATA_DIR, { recursive: true });
22 | } catch {}
23 | }
24 |
25 | export async function loadHistory() {
26 | await ensureDataDir();
27 | try {
28 | const data = await fs.readFile(HISTORY_FILE, 'utf-8');
29 | const parsed = JSON.parse(data);
30 | history = new Map(Object.entries(parsed).map(([k, v]) => [k, v as Interaction[]]));
31 | } catch {
32 | history.set('default', []);
33 | }
34 | }
35 |
36 | async function saveHistory() {
37 | const data = Object.fromEntries(history);
38 | await fs.writeFile(HISTORY_FILE, JSON.stringify(data));
39 | }
40 |
41 | export function getHistorySummary(sessionId = 'default'): string {
42 | const sessHistory = history.get(sessionId) || [];
43 | if (!sessHistory.length) return '';
44 | const summary = sessHistory.slice(-5).map((int, i) => `Interaction ${i+1}: Goal ${int.input.goal}, Guidance: ${int.output.slice(0, 100)}...`).join('\n');
45 | return `History Context:\n${summary}\n`;
46 | }
47 |
48 | export function addToHistory(sessionId = 'default', input: VibeCheckInput, output: string) {
49 | if (!history.has(sessionId)) {
50 | history.set(sessionId, []);
51 | }
52 | const sessHistory = history.get(sessionId)!;
53 | sessHistory.push({ input, output, timestamp: Date.now() });
54 | if (sessHistory.length > 10) {
55 | sessHistory.shift();
56 | }
57 | saveHistory();
58 | }
59 |
60 |
```
--------------------------------------------------------------------------------
/docs/release-workflows.md:
--------------------------------------------------------------------------------
```markdown
1 | # Release & versioning workflow
2 |
3 | ## Source of truth
4 |
5 | - `version.json` stores the canonical semantic version for the project. Update this file first when preparing a release.
6 | - `scripts/sync-version.mjs` reads `version.json` and synchronizes `package.json`, `package-lock.json`, `README.md`, and the primary `CHANGELOG.md` headers.
7 |
8 | ## Syncing metadata
9 |
10 | 1. Update `version.json` with the next version.
11 | 2. Run `npm run sync-version` to apply the version across metadata, README badges, and the changelog title.
12 | 3. Inspect the diff to ensure the package manifests and documentation updated as expected.
13 |
14 | > Tip: `npm run sync-version` will validate the version string and exit non-zero if the value is not compliant `major.minor.patch` semver.
15 |
16 | ## Changelog updates
17 |
18 | - Summarize notable changes under the "## Unreleased" section in [`CHANGELOG.md`](../CHANGELOG.md), then rename it to match the release tag (for example `## v2.8.0 - 2025-11-04`).
19 | - Mirror the highlights in [`docs/changelog.md`](./changelog.md) if you maintain the curated public history.
20 |
21 | ## npm workflows
22 |
23 | - `npm run prepublishOnly` automatically runs `npm run build` before `npm publish`, ensuring the transpiled output is current.
24 | - `npm publish` should only be executed after the sync step so the registry receives the correct version number.
25 | - Use `npm pack` or `npm publish --dry-run` to verify the release contents locally when iterating on the workflow.
26 |
27 | ## Checklist
28 |
29 | - [ ] Update `version.json`
30 | - [ ] `npm run sync-version`
31 | - [ ] Update changelog entries (`CHANGELOG.md`, optional `docs/changelog.md`)
32 | - [ ] `npm test` (or relevant verification)
33 | - [ ] `npm publish` (after confirming `npm run prepublishOnly` completes successfully)
34 |
```
--------------------------------------------------------------------------------
/docs/api-keys.md:
--------------------------------------------------------------------------------
```markdown
1 | # API Keys & Secret Management
2 |
3 | Vibe Check MCP works with multiple LLM providers. Use this guide to decide which keys you need, how they are discovered, where they are stored, and how to keep them secure.
4 |
5 | ## Supported providers
6 |
7 | - **Anthropic** – `ANTHROPIC_API_KEY`
8 | - **Google Gemini** – `GEMINI_API_KEY`
9 | - **OpenAI** – `OPENAI_API_KEY`
10 | - **OpenRouter** – `OPENROUTER_API_KEY`
11 |
12 | Only one key is required to run the server, but you can set more than one to enable provider switching.
13 |
14 | ## Secret resolution order
15 |
16 | When the CLI or server looks up a provider key it evaluates sources in the following order:
17 |
18 | 1. Existing process environment variables.
19 | 2. The current project's `.env` file (pass `--local` to write here).
20 | 3. Your home directory config at `~/.vibe-check/.env`.
21 |
22 | The first match wins, so values in the shell take priority over project-level overrides, which in turn take priority over the persisted home config.
23 |
24 | ## Storage locations
25 |
26 | - Interactive CLI runs create or update `~/.vibe-check/.env` with `0600` permissions so only your user can read it.
27 | - Supplying `--local` targets the project `.env`, letting you keep per-repository overrides under version control ignore rules.
28 | - Non-interactive runs expect keys to already be available in the environment or the relevant `.env` file; they will exit early if a required key is missing.
29 |
30 | ## Security recommendations
31 |
32 | - Treat `.env` files as secrets: keep them out of version control and shared storage.
33 | - Use the minimal set of provider keys required for your workflow and rotate them periodically.
34 | - Prefer scoped or workspace-specific keys when your provider supports them.
35 | - Restrict file permissions to your user (the CLI enforces this for the home config) and avoid copying secrets into client config files.
36 |
```
--------------------------------------------------------------------------------
/scripts/security-check.cjs:
--------------------------------------------------------------------------------
```
1 | const { execSync } = require('child_process');
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | function runAudit() {
6 | try {
7 | const output = execSync('npm audit --production --json', { encoding: 'utf8' });
8 | const json = JSON.parse(output);
9 | const vulnerabilities = json.vulnerabilities || {};
10 | let highOrCritical = 0;
11 | for (const name of Object.keys(vulnerabilities)) {
12 | const v = vulnerabilities[name];
13 | if (['high', 'critical'].includes(v.severity)) {
14 | console.error(`High severity issue in dependency: ${name}`);
15 | highOrCritical++;
16 | }
17 | }
18 | if (highOrCritical > 0) {
19 | console.error(`Found ${highOrCritical} high or critical vulnerabilities`);
20 | process.exitCode = 1;
21 | } else {
22 | console.log('Dependency audit clean');
23 | }
24 | } catch (err) {
25 | console.error('npm audit failed', err.message);
26 | process.exitCode = 1;
27 | }
28 | }
29 |
30 | function scanSource() {
31 | const suspiciousPatterns = [/eval\s*\(/, /child_process/, /exec\s*\(/, /spawn\s*\(/];
32 | let flagged = false;
33 | function scanDir(dir) {
34 | for (const file of fs.readdirSync(dir)) {
35 | const full = path.join(dir, file);
36 | const stat = fs.statSync(full);
37 | if (stat.isDirectory()) {
38 | scanDir(full);
39 | } else if ((full.endsWith('.ts') || full.endsWith('.js')) && !full.includes('scripts/security-check.js')) {
40 | const content = fs.readFileSync(full, 'utf8');
41 | for (const pattern of suspiciousPatterns) {
42 | if (pattern.test(content)) {
43 | console.error(`Suspicious pattern ${pattern} found in ${full}`);
44 | flagged = true;
45 | }
46 | }
47 | }
48 | }
49 | }
50 | scanDir('src');
51 | if (flagged) {
52 | process.exitCode = 1;
53 | } else {
54 | console.log('Source scan clean');
55 | }
56 | }
57 |
58 | runAudit();
59 | scanSource();
60 |
```
--------------------------------------------------------------------------------
/tests/cli-doctor-node.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
2 | import { format } from 'node:util';
3 | import { createCliProgram } from '../src/cli/index.js';
4 | import * as doctor from '../src/cli/doctor.js';
5 |
6 | function captureLogs() {
7 | const info: string[] = [];
8 | const warnings: string[] = [];
9 |
10 | const logSpy = vi.spyOn(console, 'log').mockImplementation((message?: unknown, ...rest: unknown[]) => {
11 | info.push(format(String(message ?? ''), ...rest));
12 | });
13 |
14 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation((message?: unknown, ...rest: unknown[]) => {
15 | warnings.push(format(String(message ?? ''), ...rest));
16 | });
17 |
18 | return { info, warnings, restore: () => { logSpy.mockRestore(); warnSpy.mockRestore(); } };
19 | }
20 |
21 | describe('cli doctor node version reporting', () => {
22 | beforeEach(() => {
23 | process.exitCode = undefined;
24 | });
25 |
26 | afterEach(() => {
27 | vi.restoreAllMocks();
28 | });
29 |
30 | it('reports ok when version satisfies requirement', async () => {
31 | vi.spyOn(doctor, 'checkNodeVersion').mockReturnValue({ ok: true, current: 'v20.5.0' });
32 | vi.spyOn(doctor, 'detectEnvFiles').mockReturnValue({ cwdEnv: null, homeEnv: null });
33 |
34 | const { info, warnings, restore } = captureLogs();
35 | const program = createCliProgram();
36 |
37 | await program.parseAsync(['node', 'vibe-check-mcp', 'doctor']);
38 |
39 | restore();
40 | expect(warnings).toHaveLength(0);
41 | expect(info.join('\n')).toContain('meets');
42 | expect(process.exitCode).toBeUndefined();
43 | });
44 |
45 | it('warns when version is below requirement', async () => {
46 | vi.spyOn(doctor, 'checkNodeVersion').mockReturnValue({ ok: false, current: 'v18.0.0' });
47 | vi.spyOn(doctor, 'detectEnvFiles').mockReturnValue({ cwdEnv: null, homeEnv: null });
48 |
49 | const { warnings, restore } = captureLogs();
50 | const program = createCliProgram();
51 |
52 | await program.parseAsync(['node', 'vibe-check-mcp', 'doctor']);
53 |
54 | restore();
55 | expect(warnings.join('\n')).toContain('requires');
56 | expect(process.exitCode).toBe(1);
57 | });
58 | });
59 |
```
--------------------------------------------------------------------------------
/tests/vibeCheck.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { vi, describe, it, expect, beforeEach } from 'vitest';
2 | import { vibeCheckTool } from '../src/tools/vibeCheck.js';
3 | import * as llm from '../src/utils/llm.js';
4 | import * as state from '../src/utils/state.js';
5 |
6 | vi.mock('../src/utils/llm.js');
7 | vi.mock('../src/utils/state.js');
8 |
9 | const mockedLLM = llm as unknown as { getMetacognitiveQuestions: ReturnType<typeof vi.fn> };
10 | const mockedState = state as unknown as {
11 | addToHistory: ReturnType<typeof vi.fn>;
12 | getHistorySummary: ReturnType<typeof vi.fn>;
13 | };
14 |
15 | beforeEach(() => {
16 | vi.clearAllMocks();
17 | mockedState.getHistorySummary = vi.fn().mockReturnValue('Mock history');
18 | mockedState.addToHistory = vi.fn();
19 | mockedLLM.getMetacognitiveQuestions = vi.fn().mockResolvedValue({ questions: 'Mock guidance' });
20 | });
21 |
22 | describe('vibeCheckTool', () => {
23 | it('returns questions from llm', async () => {
24 | const result = await vibeCheckTool({ goal: 'Test goal', plan: 'Test plan' });
25 | expect(result.questions).toBe('Mock guidance');
26 | expect(mockedLLM.getMetacognitiveQuestions).toHaveBeenCalledWith(
27 | expect.objectContaining({ goal: 'Test goal', plan: 'Test plan', historySummary: 'Mock history' })
28 | );
29 | });
30 |
31 | it('passes model override to llm', async () => {
32 | await vibeCheckTool({ goal: 'g', plan: 'p', modelOverride: { provider: 'openai' } });
33 | expect(mockedLLM.getMetacognitiveQuestions).toHaveBeenCalledWith(
34 | expect.objectContaining({ modelOverride: { provider: 'openai' } })
35 | );
36 | });
37 |
38 | it('adds to history on each call', async () => {
39 | await vibeCheckTool({ goal: 'A', plan: 'B', sessionId: 's1' });
40 | await vibeCheckTool({ goal: 'C', plan: 'D', sessionId: 's1' });
41 | expect(mockedState.addToHistory).toHaveBeenCalledTimes(2);
42 | });
43 |
44 | it('falls back to default questions when llm fails', async () => {
45 | mockedLLM.getMetacognitiveQuestions = vi.fn().mockRejectedValue(new Error('fail'));
46 | const result = await vibeCheckTool({ goal: 'x', plan: 'y' });
47 | expect(result.questions).toContain('Does this plan directly address');
48 | });
49 | });
50 |
```
--------------------------------------------------------------------------------
/src/cli/clients/vscode.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { join, resolve } from 'node:path';
2 | import {
3 | ClientAdapter,
4 | JsonRecord,
5 | MergeOpts,
6 | MergeResult,
7 | expandHomePath,
8 | mergeIntoMap,
9 | pathExists,
10 | readJsonFile,
11 | writeJsonFileAtomic,
12 | } from './shared.js';
13 |
14 | const locateVsCodeConfig = async (customPath?: string): Promise<string | null> => {
15 | if (customPath) {
16 | return expandHomePath(customPath);
17 | }
18 |
19 | const workspacePath = join(process.cwd(), '.vscode', 'mcp.json');
20 | if (await pathExists(workspacePath)) {
21 | return resolve(workspacePath);
22 | }
23 |
24 | return null;
25 | };
26 |
27 | const readVsCodeConfig = async (path: string, raw?: string): Promise<JsonRecord> => {
28 | return readJsonFile(path, raw);
29 | };
30 |
31 | const mergeVsCodeEntry = (config: JsonRecord, entry: JsonRecord, options: MergeOpts): MergeResult => {
32 | const baseEntry: JsonRecord = {
33 | command: entry.command,
34 | args: entry.args,
35 | env: entry.env ?? {},
36 | transport: options.transport,
37 | };
38 |
39 | if (options.transport === 'http') {
40 | baseEntry.url = options.httpUrl ?? 'http://127.0.0.1:2091';
41 | }
42 |
43 | if (options.dev?.watch || options.dev?.debug) {
44 | const dev: JsonRecord = {};
45 | if (options.dev.watch) {
46 | dev.watch = true;
47 | }
48 | if (options.dev.debug) {
49 | dev.debug = options.dev.debug;
50 | }
51 | baseEntry.dev = dev;
52 | }
53 |
54 | return mergeIntoMap(config, baseEntry, options, 'servers');
55 | };
56 |
57 | const writeVsCodeConfigAtomic = async (path: string, data: JsonRecord): Promise<void> => {
58 | await writeJsonFileAtomic(path, data);
59 | };
60 |
61 | const adapter: ClientAdapter = {
62 | locate: locateVsCodeConfig,
63 | read: readVsCodeConfig,
64 | merge: mergeVsCodeEntry,
65 | writeAtomic: writeVsCodeConfigAtomic,
66 | describe() {
67 | return {
68 | name: 'Visual Studio Code',
69 | pathHint: '.vscode/mcp.json (workspace)',
70 | summary: 'VS Code MCP configuration supporting stdio and HTTP transports.',
71 | transports: ['stdio', 'http'],
72 | defaultTransport: 'stdio',
73 | notes: 'Use the Command Palette → "MCP: Add Server" for profile installs.',
74 | docsUrl: 'https://code.visualstudio.com/docs/copilot/mcp',
75 | };
76 | },
77 | };
78 |
79 | export default adapter;
80 |
```
--------------------------------------------------------------------------------
/src/cli/clients/windsurf.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { join } from 'node:path';
2 | import os from 'node:os';
3 | import {
4 | ClientAdapter,
5 | JsonRecord,
6 | MergeOpts,
7 | MergeResult,
8 | expandHomePath,
9 | mergeIntoMap,
10 | pathExists,
11 | readJsonFile,
12 | writeJsonFileAtomic,
13 | } from './shared.js';
14 |
15 | const locateWindsurfConfig = async (customPath?: string): Promise<string | null> => {
16 | if (customPath) {
17 | return expandHomePath(customPath);
18 | }
19 |
20 | const home = os.homedir();
21 | const legacy = join(home, '.codeium', 'windsurf', 'mcp_config.json');
22 | if (await pathExists(legacy)) {
23 | return legacy;
24 | }
25 |
26 | const modern = join(home, '.codeium', 'mcp_config.json');
27 | if (await pathExists(modern)) {
28 | return modern;
29 | }
30 |
31 | return null;
32 | };
33 |
34 | const readWindsurfConfig = async (path: string, raw?: string): Promise<JsonRecord> => {
35 | return readJsonFile(path, raw);
36 | };
37 |
38 | const mergeWindsurfEntry = (config: JsonRecord, entry: JsonRecord, options: MergeOpts): MergeResult => {
39 | if (options.transport === 'http') {
40 | const httpEntry: JsonRecord = {
41 | serverUrl: options.httpUrl ?? 'http://127.0.0.1:2091',
42 | };
43 | return mergeIntoMap(config, httpEntry, options, 'mcpServers');
44 | }
45 |
46 | const stdioEntry: JsonRecord = {
47 | command: entry.command,
48 | args: entry.args,
49 | env: entry.env ?? {},
50 | };
51 |
52 | return mergeIntoMap(config, stdioEntry, options, 'mcpServers');
53 | };
54 |
55 | const writeWindsurfConfigAtomic = async (path: string, data: JsonRecord): Promise<void> => {
56 | await writeJsonFileAtomic(path, data);
57 | };
58 |
59 | const adapter: ClientAdapter = {
60 | locate: locateWindsurfConfig,
61 | read: readWindsurfConfig,
62 | merge: mergeWindsurfEntry,
63 | writeAtomic: writeWindsurfConfigAtomic,
64 | describe() {
65 | return {
66 | name: 'Windsurf',
67 | pathHint: '~/.codeium/windsurf/mcp_config.json',
68 | summary: 'Codeium Windsurf IDE with stdio and HTTP MCP transports.',
69 | transports: ['stdio', 'http'],
70 | defaultTransport: 'stdio',
71 | notes: 'Newer builds use ~/.codeium/mcp_config.json. Create an empty file to opt-in.',
72 | docsUrl: 'https://docs.codeium.com/windsurf/model-context-protocol',
73 | };
74 | },
75 | };
76 |
77 | export default adapter;
78 |
```
--------------------------------------------------------------------------------
/tests/cli-doctor-port.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
2 | import { format } from 'node:util';
3 | import net from 'node:net';
4 | import { createCliProgram } from '../src/cli/index.js';
5 |
6 | describe('cli doctor port diagnostics', () => {
7 | const ORIGINAL_ENV = { ...process.env };
8 |
9 | beforeEach(() => {
10 | process.exitCode = undefined;
11 | process.env = { ...ORIGINAL_ENV };
12 | });
13 |
14 | afterEach(() => {
15 | vi.restoreAllMocks();
16 | process.env = { ...ORIGINAL_ENV };
17 | });
18 |
19 | it('reports occupied ports as in-use', async () => {
20 | const server = net.createServer();
21 | await new Promise<void>((resolve, reject) => {
22 | server.once('error', reject);
23 | server.listen({ port: 0, host: '127.0.0.1' }, () => resolve());
24 | });
25 |
26 | const address = server.address();
27 | if (typeof address !== 'object' || address === null) {
28 | throw new Error('Failed to acquire port for test');
29 | }
30 |
31 | const port = address.port;
32 | const logs: string[] = [];
33 | const logSpy = vi.spyOn(console, 'log').mockImplementation((message?: unknown, ...rest: unknown[]) => {
34 | logs.push(format(String(message ?? ''), ...rest));
35 | });
36 |
37 | const program = createCliProgram();
38 | process.env.MCP_TRANSPORT = 'http';
39 | await program.parseAsync(['node', 'vibe-check-mcp', 'doctor', '--port', String(port)]);
40 |
41 | logSpy.mockRestore();
42 | await new Promise<void>((resolve) => server.close(() => resolve()));
43 |
44 | const output = logs.join('\n');
45 | expect(output).toContain(`HTTP port ${port}: in-use`);
46 | expect(process.exitCode).toBeUndefined();
47 | });
48 |
49 | it('skips port diagnostics when using stdio transport', async () => {
50 | const logs: string[] = [];
51 | const logSpy = vi.spyOn(console, 'log').mockImplementation((message?: unknown, ...rest: unknown[]) => {
52 | logs.push(format(String(message ?? ''), ...rest));
53 | });
54 |
55 | const program = createCliProgram();
56 | delete process.env.MCP_TRANSPORT;
57 | await program.parseAsync(['node', 'vibe-check-mcp', 'doctor']);
58 |
59 | logSpy.mockRestore();
60 |
61 | expect(logs.join('\n')).toContain('Using stdio transport; port checks skipped.');
62 | });
63 | });
64 |
```
--------------------------------------------------------------------------------
/tests/cli-start-flags.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
2 | import { format } from 'node:util';
3 | import { createCliProgram } from '../src/cli/index.js';
4 |
5 | function collectLogs() {
6 | const logs: string[] = [];
7 | const logSpy = vi.spyOn(console, 'log').mockImplementation((message?: unknown, ...rest: unknown[]) => {
8 | logs.push(format(String(message ?? ''), ...rest));
9 | });
10 |
11 | return { logs, restore: () => logSpy.mockRestore() };
12 | }
13 |
14 | describe('cli start command flags', () => {
15 | const ORIGINAL_ENV = { ...process.env };
16 |
17 | beforeEach(() => {
18 | process.exitCode = undefined;
19 | process.env = { ...ORIGINAL_ENV };
20 | });
21 |
22 | afterEach(() => {
23 | vi.restoreAllMocks();
24 | process.env = { ...ORIGINAL_ENV };
25 | });
26 |
27 | async function runStartCommand(...args: string[]): Promise<string> {
28 | const { logs, restore } = collectLogs();
29 | const program = createCliProgram();
30 |
31 | await program.parseAsync(['node', 'vibe-check-mcp', 'start', ...args, '--dry-run']);
32 |
33 | restore();
34 | return logs.join('\n');
35 | }
36 |
37 | it('defaults to stdio when no flag or env is provided', async () => {
38 | delete process.env.MCP_TRANSPORT;
39 |
40 | const output = await runStartCommand();
41 | expect(output).toContain('MCP_TRANSPORT=stdio');
42 | expect(output).not.toContain('MCP_HTTP_PORT');
43 | });
44 |
45 | it('preserves MCP_TRANSPORT from the environment when no flag is set', async () => {
46 | process.env.MCP_TRANSPORT = 'http';
47 |
48 | const output = await runStartCommand();
49 | expect(output).toContain('MCP_TRANSPORT=http');
50 | expect(output).toContain('MCP_HTTP_PORT=2091');
51 | });
52 |
53 | it('allows CLI flags to override MCP_TRANSPORT from the environment', async () => {
54 | process.env.MCP_TRANSPORT = 'http';
55 |
56 | const output = await runStartCommand('--stdio');
57 | expect(output).toContain('MCP_TRANSPORT=stdio');
58 | expect(output).not.toContain('MCP_HTTP_PORT');
59 | });
60 |
61 | it('prints http transport and chosen port during dry run', async () => {
62 | const output = await runStartCommand('--http', '--port', '1234');
63 | expect(output).toContain('MCP_TRANSPORT=http');
64 | expect(output).toContain('MCP_HTTP_PORT=1234');
65 | });
66 | });
67 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@pv-bhat/vibe-check-mcp",
3 | "mcpName": "io.github.PV-Bhat/vibe-check-mcp-server",
4 | "version": "2.7.6",
5 | "description": "Metacognitive AI agent oversight: adaptive CPI interrupts for alignment, reflection and safety",
6 | "main": "build/index.js",
7 | "type": "module",
8 | "packageManager": "[email protected]",
9 | "bin": {
10 | "vibe-check-mcp": "build/cli/index.js"
11 | },
12 | "files": [
13 | "build"
14 | ],
15 | "scripts": {
16 | "build": "tsc && node -e \"const fs=require('fs');['build/index.js','build/cli/index.js'].forEach((f)=>{if(fs.existsSync(f)){fs.chmodSync(f,0o755);}})\"",
17 | "prepare": "npm run build",
18 | "start": "node build/index.js",
19 | "dev": "tsc-watch --onSuccess \"node build/index.js\"",
20 | "pretest": "npm run build",
21 | "test": "vitest run",
22 | "test:coverage": "vitest run --coverage",
23 | "security-check": "node scripts/security-check.cjs",
24 | "prepublishOnly": "npm run build",
25 | "sync-version": "node scripts/sync-version.mjs"
26 | },
27 | "dependencies": {
28 | "@google/generative-ai": "^0.17.1",
29 | "@modelcontextprotocol/sdk": "^1.16.0",
30 | "axios": "^1.12.2",
31 | "body-parser": "^1.20.2",
32 | "commander": "^12.1.0",
33 | "cors": "^2.8.5",
34 | "diff": "^5.2.0",
35 | "dotenv": "^16.4.7",
36 | "execa": "^9.5.1",
37 | "express": "^4.19.2",
38 | "openai": "^4.68.1",
39 | "semver": "^7.6.3"
40 | },
41 | "devDependencies": {
42 | "@types/cors": "^2.8.17",
43 | "@types/diff": "^7.0.2",
44 | "@types/express": "^4.17.21",
45 | "@types/node": "^20.17.25",
46 | "@types/semver": "^7.7.1",
47 | "@vitest/coverage-v8": "^3.2.4",
48 | "tsc-watch": "^6.0.0",
49 | "typescript": "^5.3.0",
50 | "vitest": "^3.2.4"
51 | },
52 | "engines": {
53 | "node": ">=20.0.0"
54 | },
55 | "keywords": [
56 | "mcp",
57 | "mcp-server",
58 | "vibe-check",
59 | "vibe-coding",
60 | "metacognition",
61 | "ai-alignment",
62 | "llm-agents",
63 | "autonomous-agents",
64 | "reflection",
65 | "agent-oversight",
66 | "ai-safety",
67 | "prompt-engineering"
68 | ],
69 | "author": "PV Bhat",
70 | "repository": {
71 | "type": "git",
72 | "url": "https://github.com/PV-Bhat/vibe-check-mcp-server.git"
73 | },
74 | "bugs": {
75 | "url": "https://github.com/PV-Bhat/vibe-check-mcp-server/issues"
76 | },
77 | "homepage": "https://github.com/PV-Bhat/vibe-check-mcp-server#readme",
78 | "license": "MIT",
79 | "publishConfig": {
80 | "access": "public"
81 | }
82 | }
83 |
```
--------------------------------------------------------------------------------
/src/cli/clients/claude-code.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { join } from 'node:path';
2 | import os from 'node:os';
3 | import {
4 | ClientAdapter,
5 | JsonRecord,
6 | MergeOpts,
7 | MergeResult,
8 | expandHomePath,
9 | mergeIntoMap,
10 | pathExists,
11 | readJsonFile,
12 | writeJsonFileAtomic,
13 | } from './shared.js';
14 |
15 | const locateClaudeCodeConfig = async (customPath?: string): Promise<string | null> => {
16 | if (customPath) {
17 | return expandHomePath(customPath);
18 | }
19 |
20 | const home = os.homedir();
21 | const candidates: string[] = [];
22 |
23 | if (process.platform === 'darwin') {
24 | candidates.push(join(home, 'Library', 'Application Support', 'Claude', 'claude_code_config.json'));
25 | } else if (process.platform === 'win32') {
26 | const appData = process.env.APPDATA;
27 | if (appData) {
28 | candidates.push(join(appData, 'Claude', 'claude_code_config.json'));
29 | }
30 | } else {
31 | const xdgConfig = process.env.XDG_CONFIG_HOME;
32 | if (xdgConfig) {
33 | candidates.push(join(xdgConfig, 'Claude', 'claude_code_config.json'));
34 | }
35 | candidates.push(join(home, '.config', 'Claude', 'claude_code_config.json'));
36 | }
37 |
38 | for (const candidate of candidates) {
39 | if (await pathExists(candidate)) {
40 | return candidate;
41 | }
42 | }
43 |
44 | return null;
45 | };
46 |
47 | const readClaudeCodeConfig = async (path: string, raw?: string): Promise<JsonRecord> => {
48 | return readJsonFile(path, raw, 'Claude Code config');
49 | };
50 |
51 | const writeClaudeCodeConfigAtomic = async (path: string, data: JsonRecord): Promise<void> => {
52 | await writeJsonFileAtomic(path, data);
53 | };
54 |
55 | const mergeClaudeCodeEntry = (config: JsonRecord, entry: JsonRecord, options: MergeOpts): MergeResult => {
56 | return mergeIntoMap(config, entry, options, 'mcpServers');
57 | };
58 |
59 | const adapter: ClientAdapter = {
60 | locate: locateClaudeCodeConfig,
61 | read: readClaudeCodeConfig,
62 | merge: mergeClaudeCodeEntry,
63 | writeAtomic: writeClaudeCodeConfigAtomic,
64 | describe() {
65 | return {
66 | name: 'Claude Code',
67 | pathHint: '~/.config/Claude/claude_code_config.json',
68 | summary: 'Anthropic\'s Claude Code CLI agent configuration.',
69 | transports: ['stdio'],
70 | defaultTransport: 'stdio',
71 | requiredEnvKeys: ['ANTHROPIC_API_KEY'],
72 | notes: 'Run `claude code login` once to scaffold the config file.',
73 | docsUrl: 'https://docs.anthropic.com/en/docs/agents/claude-code',
74 | };
75 | },
76 | };
77 |
78 | export default adapter;
79 |
```
--------------------------------------------------------------------------------
/src/tools/vibeCheck.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { getMetacognitiveQuestions } from '../utils/llm.js';
2 | import { addToHistory, getHistorySummary } from '../utils/state.js';
3 |
4 | // Vibe Check tool handler
5 | export interface VibeCheckInput {
6 | goal: string;
7 | plan: string;
8 | modelOverride?: {
9 | provider?: string;
10 | model?: string;
11 | };
12 | userPrompt?: string;
13 | progress?: string;
14 | uncertainties?: string[];
15 | taskContext?: string;
16 | sessionId?: string;
17 | }
18 |
19 | export interface VibeCheckOutput {
20 | questions: string;
21 | }
22 |
23 | /**
24 | * Adaptive CPI interrupt for AI agent alignment and reflection.
25 | * Monitors progress and questions assumptions to mitigate Reasoning Lock-In.
26 | * The userRequest parameter MUST contain the full original request for safety.
27 | */
28 | export async function vibeCheckTool(input: VibeCheckInput): Promise<VibeCheckOutput> {
29 | console.log('[vibe_check] called', { hasSession: Boolean(input.sessionId) });
30 | try {
31 | // Get history summary
32 | const historySummary = getHistorySummary(input.sessionId);
33 |
34 | // Get metacognitive questions from Gemini with dynamic parameters
35 | const response = await getMetacognitiveQuestions({
36 | goal: input.goal,
37 | plan: input.plan,
38 | modelOverride: input.modelOverride,
39 | userPrompt: input.userPrompt,
40 | progress: input.progress,
41 | uncertainties: input.uncertainties,
42 | taskContext: input.taskContext,
43 | sessionId: input.sessionId,
44 | historySummary,
45 | });
46 |
47 | // Add to history
48 | addToHistory(input.sessionId, input, response.questions);
49 |
50 | return {
51 | questions: response.questions,
52 | };
53 | } catch (error) {
54 | console.error('Error in vibe_check tool:', error);
55 |
56 | // Fallback to basic questions if there's an error
57 | return {
58 | questions: generateFallbackQuestions(input.userPrompt || "", input.plan || ""),
59 | };
60 | }
61 | }
62 |
63 | /**
64 | * Generate adaptive fallback questions when API fails
65 | */
66 | function generateFallbackQuestions(userRequest: string, plan: string): string {
67 | return `
68 | I can see you're thinking through your approach, which shows thoughtfulness:
69 |
70 | 1. Does this plan directly address what the user requested, or might it be solving a different problem?
71 | 2. Is there a simpler approach that would meet the user's needs?
72 | 3. What unstated assumptions might be limiting the thinking here?
73 | 4. How does this align with the user's original intent?
74 | `;
75 | }
```
--------------------------------------------------------------------------------
/tests/llm.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
2 | import axios from 'axios';
3 | import { generateResponse, __testing } from '../src/utils/llm.js';
4 |
5 | vi.mock('axios');
6 | const mockedAxios = axios as unknown as { post: ReturnType<typeof vi.fn> };
7 |
8 | beforeEach(() => {
9 | vi.clearAllMocks();
10 | __testing.setGenAI({
11 | getGenerativeModel: vi.fn(() => ({
12 | generateContent: vi.fn(async () => ({ response: { text: () => 'gemini reply' } }))
13 | }))
14 | });
15 | __testing.setOpenAIClient({
16 | chat: { completions: { create: vi.fn(async () => ({ choices: [{ message: { content: 'openai reply' } }] })) } }
17 | });
18 | });
19 |
20 | describe('generateResponse', () => {
21 | it('uses gemini by default and builds prompt with context', async () => {
22 | const res = await generateResponse({ goal: 'G', plan: 'P', uncertainties: ['u1'], historySummary: 'Hist' });
23 | expect(res.questions).toBe('gemini reply');
24 | const gen = __testing.getGenAI();
25 | expect(gen.getGenerativeModel).toHaveBeenCalledWith({ model: 'gemini-2.5-pro' });
26 | const prompt = gen.getGenerativeModel.mock.results[0].value.generateContent.mock.calls[0][0];
27 | expect(prompt).toContain('History Context: Hist');
28 | expect(prompt).toContain('u1');
29 | });
30 |
31 | it('uses openai when overridden', async () => {
32 | const openai = __testing.getOpenAIClient();
33 | const res = await generateResponse({ goal: 'g', plan: 'p', modelOverride: { provider: 'openai', model: 'o1-mini' } });
34 | expect(res.questions).toBe('openai reply');
35 | expect(openai.chat.completions.create).toHaveBeenCalledWith({ model: 'o1-mini', messages: [{ role: 'system', content: expect.any(String) }] });
36 | });
37 |
38 | it('throws if openrouter key missing', async () => {
39 | await expect(generateResponse({ goal: 'g', plan: 'p', modelOverride: { provider: 'openrouter', model: 'm1' } })).rejects.toThrow('OpenRouter API key');
40 | });
41 |
42 | it('calls openrouter when configured', async () => {
43 | process.env.OPENROUTER_API_KEY = 'sk-or-key';
44 | mockedAxios.post = vi.fn(async () => ({ data: { choices: [{ message: { content: 'router reply' } }] } }));
45 | const res = await generateResponse({ goal: 'g', plan: 'p', modelOverride: { provider: 'openrouter', model: 'm1' } });
46 | expect(res.questions).toBe('router reply');
47 | expect(mockedAxios.post).toHaveBeenCalled();
48 | delete process.env.OPENROUTER_API_KEY;
49 | });
50 | });
51 |
```
--------------------------------------------------------------------------------
/src/cli/clients/claude.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { join } from 'node:path';
2 | import os from 'node:os';
3 | import {
4 | ClientAdapter,
5 | JsonRecord,
6 | MergeOpts,
7 | MergeResult,
8 | expandHomePath,
9 | mergeIntoMap,
10 | pathExists,
11 | readJsonFile,
12 | writeJsonFileAtomic,
13 | } from './shared.js';
14 |
15 | type LocateFn = (customPath?: string) => Promise<string | null>;
16 |
17 | type ReadFn = (path: string, raw?: string) => Promise<JsonRecord>;
18 |
19 | type MergeFn = (config: JsonRecord, entry: JsonRecord, options: MergeOpts) => MergeResult;
20 |
21 | type WriteFn = (path: string, data: JsonRecord) => Promise<void>;
22 |
23 | export const locateClaudeConfig: LocateFn = async (customPath) => {
24 | if (customPath) {
25 | return expandHomePath(customPath);
26 | }
27 |
28 | const home = os.homedir();
29 | const candidates: string[] = [];
30 |
31 | if (process.platform === 'darwin') {
32 | candidates.push(join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'));
33 | } else if (process.platform === 'win32') {
34 | const appData = process.env.APPDATA;
35 | if (appData) {
36 | candidates.push(join(appData, 'Claude', 'claude_desktop_config.json'));
37 | }
38 | } else {
39 | const xdgConfig = process.env.XDG_CONFIG_HOME;
40 | if (xdgConfig) {
41 | candidates.push(join(xdgConfig, 'Claude', 'claude_desktop_config.json'));
42 | }
43 | candidates.push(join(home, '.config', 'Claude', 'claude_desktop_config.json'));
44 | }
45 |
46 | for (const candidate of candidates) {
47 | if (await pathExists(candidate)) {
48 | return candidate;
49 | }
50 | }
51 |
52 | return null;
53 | };
54 |
55 | export const readClaudeConfig: ReadFn = async (path, raw) => {
56 | return readJsonFile(path, raw, 'Claude config');
57 | };
58 |
59 | export const writeClaudeConfigAtomic: WriteFn = async (path, data) => {
60 | await writeJsonFileAtomic(path, data);
61 | };
62 |
63 | export const mergeMcpEntry: MergeFn = (config, entry, options) => {
64 | return mergeIntoMap(config, entry, options, 'mcpServers');
65 | };
66 |
67 | const adapter: ClientAdapter = {
68 | locate: locateClaudeConfig,
69 | read: readClaudeConfig,
70 | merge: mergeMcpEntry,
71 | writeAtomic: writeClaudeConfigAtomic,
72 | describe() {
73 | return {
74 | name: 'Claude Desktop',
75 | pathHint: 'claude_desktop_config.json',
76 | summary: 'Claude Desktop app integration for Windows, macOS, and Linux.',
77 | transports: ['stdio'],
78 | defaultTransport: 'stdio',
79 | requiredEnvKeys: ['ANTHROPIC_API_KEY'],
80 | notes: 'Launch Claude Desktop once to generate the config file.',
81 | docsUrl: 'https://docs.anthropic.com/en/docs/claude-desktop/model-context-protocol',
82 | };
83 | },
84 | };
85 |
86 | export default adapter;
87 |
```
--------------------------------------------------------------------------------
/examples/cpi-integration.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Example CPI integration stub for VibeCheck MCP.
3 | *
4 | * Wire this into your agent orchestrator to forward VibeCheck signals to a CPI policy.
5 | */
6 |
7 | export interface AgentSnapshot {
8 | sessionId: string;
9 | summary: string;
10 | nextAction: string;
11 | done?: boolean;
12 | }
13 |
14 | export interface ResumeSignal {
15 | reason: string;
16 | followUp?: string;
17 | }
18 |
19 | export interface AgentStepCallback {
20 | (input: { resumeSignal?: ResumeSignal }): Promise<AgentSnapshot>;
21 | }
22 |
23 | export interface VibeCheckSignal {
24 | riskScore: number;
25 | traits: string[];
26 | advice: string;
27 | }
28 |
29 | const RISK_THRESHOLD = 0.6;
30 |
31 | const vibecheckShim = {
32 | // TODO: replace with an actual call to the VibeCheck MCP tool over MCP or HTTP.
33 | async analyze(snapshot: AgentSnapshot): Promise<VibeCheckSignal> {
34 | return {
35 | riskScore: Math.random(),
36 | traits: ['focus-drift'],
37 | advice: `Reflect on: ${snapshot.summary}`,
38 | };
39 | },
40 | };
41 |
42 | // TODO: replace with `import { createPolicy } from '@cpi/sdk';`
43 | const cpiPolicyShim = {
44 | interrupt(input: { snapshot: AgentSnapshot; signal: VibeCheckSignal }) {
45 | if (input.signal.riskScore >= RISK_THRESHOLD) {
46 | return {
47 | action: 'interrupt' as const,
48 | reason: 'High metacognitive risk detected by VibeCheck',
49 | };
50 | }
51 |
52 | return { action: 'allow' as const };
53 | },
54 | };
55 |
56 | async function handleInterrupt(
57 | decision: { action: 'interrupt' | 'allow'; reason?: string },
58 | snapshot: AgentSnapshot,
59 | ): Promise<ResumeSignal | undefined> {
60 | if (decision.action === 'allow') {
61 | return undefined;
62 | }
63 |
64 | console.warn('[CPI] interrupting agent step:', decision.reason ?? 'policy requested pause');
65 | console.warn('Agent summary:', snapshot.summary);
66 |
67 | // TODO: replace with human-in-the-loop logic or CPI repro harness callback.
68 | return {
69 | reason: decision.reason ?? 'Paused for inspection',
70 | followUp: 'Agent acknowledged CPI feedback and is ready to resume.',
71 | };
72 | }
73 |
74 | export async function runWithCPI(agentStep: AgentStepCallback): Promise<void> {
75 | let resumeSignal: ResumeSignal | undefined;
76 |
77 | while (true) {
78 | const snapshot = await agentStep({ resumeSignal });
79 |
80 | if (snapshot.done) {
81 | console.log('Agent workflow completed.');
82 | break;
83 | }
84 |
85 | const signal = await vibecheckShim.analyze(snapshot);
86 | console.log('VibeCheck signal', signal);
87 |
88 | const decision = cpiPolicyShim.interrupt({ snapshot, signal });
89 |
90 | if (decision.action !== 'allow') {
91 | resumeSignal = await handleInterrupt(decision, snapshot);
92 | continue;
93 | }
94 |
95 | resumeSignal = undefined;
96 | }
97 | }
98 |
```
--------------------------------------------------------------------------------
/src/utils/jsonRpcCompat.ts:
--------------------------------------------------------------------------------
```typescript
1 | import crypto from 'node:crypto';
2 |
3 | export interface JsonRpcRequestLike {
4 | jsonrpc?: unknown;
5 | id?: unknown;
6 | method?: unknown;
7 | params?: unknown;
8 | }
9 |
10 | const STABLE_ID_PREFIX = 'compat-';
11 |
12 | function computeStableHash(request: JsonRpcRequestLike): string {
13 | const params = request.params ?? {};
14 | return crypto
15 | .createHash('sha256')
16 | .update(JSON.stringify({ method: request.method, params }))
17 | .digest('hex')
18 | .slice(0, 12);
19 | }
20 |
21 | function generateNonce(): string {
22 | const nonceValue = parseInt(crypto.randomBytes(3).toString('hex'), 16);
23 | const base36 = nonceValue.toString(36);
24 | return base36.padStart(4, '0').slice(-6);
25 | }
26 |
27 | export function formatCompatId(stableHash: string, nonce: string = generateNonce()): string {
28 | return `${STABLE_ID_PREFIX}${stableHash}-${nonce}`;
29 | }
30 |
31 | function computeStableId(request: JsonRpcRequestLike): string {
32 | const stableHash = computeStableHash(request);
33 | return formatCompatId(stableHash);
34 | }
35 |
36 | export interface ShimResult {
37 | applied: boolean;
38 | id?: string;
39 | }
40 |
41 | export function applyJsonRpcCompatibility(request: JsonRpcRequestLike | undefined | null): ShimResult {
42 | if (!request || typeof request !== 'object') {
43 | return { applied: false };
44 | }
45 |
46 | if ((request as any).jsonrpc !== '2.0') {
47 | return { applied: false };
48 | }
49 |
50 | if ((request as any).method !== 'tools/call') {
51 | return { applied: false };
52 | }
53 |
54 | if ((request as any).id !== undefined && (request as any).id !== null) {
55 | return { applied: false };
56 | }
57 |
58 | const id = computeStableId(request);
59 | (request as any).id = id;
60 | return { applied: true, id };
61 | }
62 |
63 | export interface TransportLike {
64 | onmessage?: (message: any, extra?: any) => void;
65 | }
66 |
67 | export function wrapTransportForCompatibility<T extends TransportLike>(transport: T): T {
68 | const originalOnMessage = transport.onmessage;
69 | let wrappedHandler: typeof transport.onmessage;
70 |
71 | const wrapHandler = (handler?: typeof transport.onmessage) => {
72 | if (!handler) {
73 | wrappedHandler = undefined;
74 | return;
75 | }
76 |
77 | wrappedHandler = function (this: unknown, message: JsonRpcRequestLike, extra?: any) {
78 | applyJsonRpcCompatibility(message);
79 | return handler.call(this ?? transport, message, extra);
80 | };
81 | };
82 |
83 | Object.defineProperty(transport, 'onmessage', {
84 | configurable: true,
85 | enumerable: true,
86 | get() {
87 | return wrappedHandler;
88 | },
89 | set(handler) {
90 | if (handler === wrappedHandler && handler !== undefined) {
91 | return;
92 | }
93 | wrapHandler(handler);
94 | },
95 | });
96 |
97 | if (originalOnMessage) {
98 | transport.onmessage = originalOnMessage;
99 | }
100 |
101 | return transport;
102 | }
103 |
```
--------------------------------------------------------------------------------
/scripts/install-vibe-check.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | echo "========================================================"
4 | echo "Vibe Check MCP Server Installer for Cursor IDE (Mac/Linux)"
5 | echo "========================================================"
6 | echo ""
7 |
8 | # Check for Node.js installation
9 | if ! command -v node &> /dev/null; then
10 | echo "Error: Node.js is not installed or not in PATH."
11 | echo "Please install Node.js from https://nodejs.org/"
12 | exit 1
13 | fi
14 |
15 | # Check for npm installation
16 | if ! command -v npm &> /dev/null; then
17 | echo "Error: npm is not installed or not in PATH."
18 | echo "Please install Node.js from https://nodejs.org/"
19 | exit 1
20 | fi
21 |
22 | # Detect OS
23 | OS="$(uname -s)"
24 | case "${OS}" in
25 | Linux*) OS="Linux";;
26 | Darwin*) OS="Mac";;
27 | *) OS="Unknown";;
28 | esac
29 |
30 | if [ "$OS" = "Unknown" ]; then
31 | echo "Error: Unsupported operating system. This script works on Mac and Linux only."
32 | exit 1
33 | fi
34 |
35 | echo "Step 1: Installing @pv-bhat/vibe-check-mcp globally..."
36 | npm install -g @pv-bhat/vibe-check-mcp
37 |
38 | if [ $? -ne 0 ]; then
39 | echo "Error: Failed to install @pv-bhat/vibe-check-mcp globally."
40 | exit 1
41 | fi
42 |
43 | echo ""
44 | echo "Step 2: Finding global npm installation path..."
45 | NPM_GLOBAL=$(npm root -g)
46 | VIBE_CHECK_PATH="$NPM_GLOBAL/@pv-bhat/vibe-check-mcp/build/index.js"
47 |
48 | if [ ! -f "$VIBE_CHECK_PATH" ]; then
49 | echo "Error: Could not find @pv-bhat/vibe-check-mcp installation at $VIBE_CHECK_PATH"
50 | exit 1
51 | fi
52 |
53 | echo "Found @pv-bhat/vibe-check-mcp at: $VIBE_CHECK_PATH"
54 | echo ""
55 |
56 | echo "Step 3: Enter your Gemini API key for vibe-check-mcp..."
57 | read -p "Enter your Gemini API key: " GEMINI_API_KEY
58 |
59 | # Create .env file in user's home directory
60 | echo "Creating .env file for Gemini API key..."
61 | ENV_FILE="$HOME/.vibe-check-mcp.env"
62 | echo "GEMINI_API_KEY=$GEMINI_API_KEY" > "$ENV_FILE"
63 | chmod 600 "$ENV_FILE" # Secure the API key file
64 |
65 | # Create start script
66 | START_SCRIPT="$HOME/start-vibe-check-mcp.sh"
67 | cat > "$START_SCRIPT" << EOL
68 | #!/bin/bash
69 | source "$ENV_FILE"
70 | exec node "$VIBE_CHECK_PATH"
71 | EOL
72 |
73 | chmod +x "$START_SCRIPT"
74 | echo "Created startup script: $START_SCRIPT"
75 |
76 | echo ""
77 | echo "Step 4: Setting up Cursor IDE configuration..."
78 | echo ""
79 | echo "To complete setup, you need to configure Cursor IDE:"
80 | echo ""
81 | echo "1. Open Cursor IDE"
82 | echo "2. Go to Settings (gear icon) -> MCP"
83 | echo "3. Click \"Add New MCP Server\""
84 | echo "4. Enter the following information:"
85 | echo " - Name: Vibe Check"
86 | echo " - Type: Command"
87 | echo " - Command: env GEMINI_API_KEY=$GEMINI_API_KEY node \"$VIBE_CHECK_PATH\""
88 | echo "5. Click \"Save\" and then \"Refresh\""
89 | echo ""
90 | echo "Installation complete!"
91 | echo ""
92 | echo "You can manually run it by executing: $START_SCRIPT"
93 | echo ""
```
--------------------------------------------------------------------------------
/tests/storage-utils.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { afterEach, describe, expect, it, vi } from 'vitest';
2 | import fs from 'fs';
3 | import os from 'os';
4 | import path from 'path';
5 |
6 | type StorageModule = typeof import('../src/utils/storage.js');
7 |
8 | let tempDir: string;
9 | let storage: StorageModule;
10 |
11 | async function loadStorageModule(dir: string): Promise<StorageModule> {
12 | vi.resetModules();
13 | vi.doMock('os', () => ({
14 | default: {
15 | homedir: () => dir,
16 | },
17 | }));
18 | const mod = await import('../src/utils/storage.js');
19 | return mod;
20 | }
21 |
22 | afterEach(() => {
23 | vi.doUnmock('os');
24 | if (tempDir && fs.existsSync(tempDir)) {
25 | fs.rmSync(tempDir, { recursive: true, force: true });
26 | }
27 | });
28 |
29 | describe('storage utilities', () => {
30 | it('writes and reads log data safely', async () => {
31 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-storage-test-'));
32 | storage = await loadStorageModule(tempDir);
33 |
34 | const logPath = path.join(tempDir, '.vibe-check', 'vibe-log.json');
35 |
36 | const mockLog = {
37 | mistakes: {
38 | Example: {
39 | count: 1,
40 | examples: [
41 | {
42 | type: 'mistake' as const,
43 | category: 'Example',
44 | mistake: 'Did something wrong.',
45 | solution: 'Fixed it quickly.',
46 | timestamp: Date.now(),
47 | },
48 | ],
49 | lastUpdated: Date.now(),
50 | },
51 | },
52 | lastUpdated: Date.now(),
53 | };
54 |
55 | storage.writeLogFile(mockLog);
56 | expect(fs.existsSync(logPath)).toBe(true);
57 | const readBack = storage.readLogFile();
58 | expect(readBack.mistakes.Example.count).toBe(1);
59 |
60 | fs.writeFileSync(logPath, 'not-json');
61 | const fallback = storage.readLogFile();
62 | expect(fallback.mistakes).toEqual({});
63 | });
64 |
65 | it('tracks learning entries and summaries', async () => {
66 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-storage-test-'));
67 | storage = await loadStorageModule(tempDir);
68 |
69 | storage.addLearningEntry('Missed tests', 'Feature Creep', 'Add coverage', 'mistake');
70 | storage.addLearningEntry('Shipped fast', 'Success', undefined, 'success');
71 | storage.addLearningEntry('Too many tools', 'Overtooling', 'Simplify stack', 'mistake');
72 |
73 | const entries = storage.getLearningEntries();
74 | expect(Object.keys(entries)).toEqual(expect.arrayContaining(['Feature Creep', 'Success', 'Overtooling']));
75 |
76 | const summary = storage.getLearningCategorySummary();
77 | expect(summary[0].count).toBeGreaterThan(0);
78 | expect(summary.some((item) => item.category === 'Feature Creep')).toBe(true);
79 |
80 | const context = storage.getLearningContextText(2);
81 | expect(context).toContain('Category: Feature Creep');
82 | expect(context).toContain('Mistake');
83 | });
84 | });
85 |
```
--------------------------------------------------------------------------------
/scripts/sync-version.mjs:
--------------------------------------------------------------------------------
```
1 | import { readFile, writeFile } from 'node:fs/promises';
2 | import { dirname, join } from 'node:path';
3 | import { fileURLToPath } from 'node:url';
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 | const rootDir = join(__dirname, '..');
8 |
9 | const resolvePath = (relativePath) => join(rootDir, relativePath);
10 |
11 | const loadJson = async (relativePath) => {
12 | const filePath = resolvePath(relativePath);
13 | const contents = await readFile(filePath, 'utf8');
14 | return { filePath, data: JSON.parse(contents) };
15 | };
16 |
17 | const writeJson = async (filePath, data) => {
18 | const serialized = JSON.stringify(data, null, 2) + '\n';
19 | await writeFile(filePath, serialized, 'utf8');
20 | };
21 |
22 | const replaceInFile = async (relativePath, replacements) => {
23 | const filePath = resolvePath(relativePath);
24 | let contents = await readFile(filePath, 'utf8');
25 | for (const { pattern, value } of replacements) {
26 | contents = contents.replace(pattern, value);
27 | }
28 | await writeFile(filePath, contents, 'utf8');
29 | };
30 |
31 | const main = async () => {
32 | const { data: versionData } = await loadJson('version.json');
33 | const newVersion = versionData.version;
34 |
35 | if (typeof newVersion !== 'string' || !/^\d+\.\d+\.\d+$/.test(newVersion)) {
36 | throw new Error(`Invalid semver version in version.json: "${newVersion}"`);
37 | }
38 |
39 | const { filePath: packageJsonPath, data: packageJson } = await loadJson('package.json');
40 | if (packageJson.version !== newVersion) {
41 | packageJson.version = newVersion;
42 | await writeJson(packageJsonPath, packageJson);
43 | }
44 |
45 | try {
46 | const { filePath: lockPath, data: packageLock } = await loadJson('package-lock.json');
47 | let updatedLock = false;
48 | if (packageLock.version !== newVersion) {
49 | packageLock.version = newVersion;
50 | updatedLock = true;
51 | }
52 | if (packageLock.packages?.['']?.version && packageLock.packages[''].version !== newVersion) {
53 | packageLock.packages[''].version = newVersion;
54 | updatedLock = true;
55 | }
56 | if (updatedLock) {
57 | await writeJson(lockPath, packageLock);
58 | }
59 | } catch (error) {
60 | if (error.code !== 'ENOENT') {
61 | throw error;
62 | }
63 | }
64 |
65 | await replaceInFile('README.md', [
66 | { pattern: /Vibe Check MCP v\d+\.\d+\.\d+/, value: `Vibe Check MCP v${newVersion}` },
67 | { pattern: /version-\d+\.\d+\.\d+-purple/, value: `version-${newVersion}-purple` },
68 | { pattern: /## What's New in v\d+\.\d+\.\d+/, value: `## What's New in v${newVersion}` }
69 | ]);
70 |
71 | await replaceInFile('CHANGELOG.md', [
72 | { pattern: /## v\d+\.\d+\.\d+ -/, value: `## v${newVersion} -` }
73 | ]);
74 |
75 | console.log(`Synchronized project files to version ${newVersion}`);
76 | };
77 |
78 | main().catch((error) => {
79 | console.error(error);
80 | process.exitCode = 1;
81 | });
82 |
```
--------------------------------------------------------------------------------
/tests/cli-install-dry-run.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promises as fs } from 'node:fs';
2 | import { readFileSync } from 'node:fs';
3 | import { dirname, join } from 'node:path';
4 | import os from 'node:os';
5 | import { format } from 'node:util';
6 | import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
7 | import { createCliProgram } from '../src/cli/index.js';
8 |
9 | type ClientFixture = {
10 | client: 'claude' | 'cursor' | 'windsurf' | 'vscode';
11 | fixture: string;
12 | fileName: string;
13 | };
14 |
15 | const FIXTURES: ClientFixture[] = [
16 | {
17 | client: 'claude',
18 | fixture: join('claude', 'config.base.json'),
19 | fileName: 'claude.json',
20 | },
21 | {
22 | client: 'cursor',
23 | fixture: join('cursor', 'config.base.json'),
24 | fileName: 'cursor.json',
25 | },
26 | {
27 | client: 'windsurf',
28 | fixture: join('windsurf', 'config.base.json'),
29 | fileName: 'mcp_config.json',
30 | },
31 | {
32 | client: 'vscode',
33 | fixture: join('vscode', 'workspace.mcp.base.json'),
34 | fileName: join('.vscode', 'mcp.json'),
35 | },
36 | ];
37 |
38 | describe('cli install --dry-run', () => {
39 | const ORIGINAL_ENV = { ...process.env };
40 |
41 | beforeEach(() => {
42 | process.exitCode = undefined;
43 | process.env = { ...ORIGINAL_ENV };
44 | });
45 |
46 | afterEach(() => {
47 | vi.restoreAllMocks();
48 | process.env = { ...ORIGINAL_ENV };
49 | });
50 |
51 | it.each(FIXTURES)('prints a unified diff without writing changes (%s)', async ({
52 | client,
53 | fixture,
54 | fileName,
55 | }) => {
56 | const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'vibe-dryrun-'));
57 | const configPath = join(tmpDir, fileName);
58 | await fs.mkdir(dirname(configPath), { recursive: true });
59 | const fixturePath = join(process.cwd(), 'tests', 'fixtures', fixture);
60 | const original = readFileSync(fixturePath, 'utf8');
61 | await fs.writeFile(configPath, original, 'utf8');
62 |
63 | process.env.ANTHROPIC_API_KEY = 'sk-ant-dry-run-key';
64 |
65 | const logs: string[] = [];
66 | const logSpy = vi.spyOn(console, 'log').mockImplementation((message?: unknown, ...rest: unknown[]) => {
67 | logs.push(format(String(message ?? ''), ...rest));
68 | });
69 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
70 |
71 | const program = createCliProgram();
72 | await program.parseAsync([
73 | 'node',
74 | 'vibe-check-mcp',
75 | 'install',
76 | '--client',
77 | client,
78 | '--config',
79 | configPath,
80 | '--dry-run',
81 | '--non-interactive',
82 | ]);
83 |
84 | logSpy.mockRestore();
85 | warnSpy.mockRestore();
86 |
87 | const joined = logs.join('\n');
88 | expect(joined).toContain('@@');
89 | expect(joined).toContain('vibe-check-mcp-cli"');
90 | expect(joined).toContain('@pv-bhat/vibe-check-mcp');
91 |
92 | const after = await fs.readFile(configPath, 'utf8');
93 | expect(after).toBe(original);
94 |
95 | const files = await fs.readdir(tmpDir);
96 | expect(files.some((file) => file.endsWith('.bak'))).toBe(false);
97 | });
98 | });
99 |
```
--------------------------------------------------------------------------------
/docs/TESTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Testing Guide
2 |
3 | The server now includes a JSON-RPC compatibility layer that backfills missing `id` fields on `tools/call` requests. The shim mitigates gaps in some clients so the stock `@modelcontextprotocol/sdk` client and Windsurf work out of the box again, but JSON-RPC 2.0-compliant clients should continue to send an `id` on every request. Treat the compatibility layer as a guard rail, not a substitute for well-formed requests.
4 |
5 | ## Running Tests
6 |
7 | 1. **Build the server:**
8 | ```bash
9 | npm run build
10 | ```
11 | 2. **Generate optional sample requests:**
12 | The legacy helper scripts remain available if you want ready-made payloads for manual testing, but they are no longer required for Windsurf or the SDK client now that the shim fills in missing identifiers.
13 | - `alt-test.js` (OpenRouter) writes `request1.json` and `request2.json` for history testing.
14 | - `alt-test-openai.js` generates `request.json` targeting the OpenAI provider.
15 | - `alt-test-gemini.js` generates `request.json` using the default Gemini provider.
16 | ```bash
17 | node alt-test.js # OpenRouter history test
18 | node alt-test-openai.js # OpenAI example
19 | node alt-test-gemini.js # Gemini example
20 | ```
21 | 3. **Run the server with the requests (optional):**
22 | Pipe the contents of each generated file to the server if you are using the helper scripts.
23 |
24 | **History test (OpenRouter):**
25 | ```bash
26 | node build/index.js < request1.json
27 | node build/index.js < request2.json
28 | ```
29 | **Single provider examples:**
30 | ```bash
31 | node build/index.js < request.json # created by alt-test-openai.js or alt-test-gemini.js
32 | ```
33 | The server will process the requests and print the responses to standard output. The second OpenRouter call should show that the previous history was considered.
34 |
35 | ## Troubleshooting
36 |
37 | - **No output after sending a request:** JSON-RPC notifications (requests without an `id`) do not receive responses by design. The compatibility shim synthesizes a stable identifier with a random nonce for `tools/call` payloads that omit `id`, but other methods still expect a proper identifier from the client. Ensure your integration includes an `id` if you need a response.
38 | - **HTTP mode confusion:** The HTTP transport now scopes JSON fallbacks to the current request. Legacy clients that only accept `application/json` receive a direct JSON response, while streaming clients continue to see Server-Sent Events. Concurrent traffic no longer causes response-mode leaks between requests.
39 |
40 | ## Unit Tests with Vitest
41 |
42 | Vitest is used for unit and integration tests. Run all tests with:
43 | ```bash
44 | npm test
45 | ```
46 | Generate a coverage report (outputs to `coverage/`):
47 | ```bash
48 | npm run test:coverage
49 | ```
50 | All tests should pass with at least 80% line coverage.
51 |
```
--------------------------------------------------------------------------------
/tests/startup.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from 'vitest';
2 | import { spawn } from 'child_process';
3 | import { fileURLToPath } from 'url';
4 | import path from 'path';
5 | import net from 'net';
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 |
10 | async function runStartupTest(envVar: 'MCP_HTTP_PORT' | 'PORT' | 'BOTH') {
11 | const startTime = Date.now();
12 |
13 | const projectRoot = path.resolve(__dirname, '..');
14 | const indexPath = path.join(projectRoot, 'build', 'index.js');
15 |
16 | const getPort = () =>
17 | new Promise<number>((resolve, reject) => {
18 | const s = net.createServer();
19 | s.listen(0, () => {
20 | const p = (s.address() as any).port;
21 | s.close(() => resolve(p));
22 | });
23 | s.on('error', reject);
24 | });
25 |
26 | const mainPort = await getPort();
27 | const env: NodeJS.ProcessEnv = { ...process.env };
28 | delete env.GEMINI_API_KEY;
29 |
30 | if (envVar === 'MCP_HTTP_PORT') {
31 | env.MCP_HTTP_PORT = String(mainPort);
32 | } else if (envVar === 'PORT') {
33 | env.PORT = String(mainPort);
34 | } else {
35 | env.MCP_HTTP_PORT = String(mainPort);
36 | const otherPort = await getPort();
37 | env.PORT = String(otherPort);
38 | }
39 |
40 | const serverProcess = spawn('node', [indexPath], {
41 | env,
42 | stdio: ['ignore', 'pipe', 'pipe'],
43 | });
44 |
45 | try {
46 | let res: Response | null = null;
47 | for (let i = 0; i < 40; i++) {
48 | try {
49 | const attempt = await fetch(`http://localhost:${mainPort}/mcp`, {
50 | method: 'POST',
51 | headers: {
52 | 'Content-Type': 'application/json',
53 | Accept: 'application/json, text/event-stream'
54 | },
55 | body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
56 | });
57 | if (attempt.status === 200) {
58 | res = attempt;
59 | break;
60 | }
61 | } catch {}
62 | await new Promise((r) => setTimeout(r, 250));
63 | }
64 | if (!res) throw new Error('Server did not start');
65 | const text = await res.text();
66 | const line = text.split('\n').find((l) => l.startsWith('data: '));
67 | const json = line ? JSON.parse(line.slice(6)) : null;
68 |
69 | const duration = Date.now() - startTime;
70 | expect(res.status).toBe(200);
71 | expect(json?.result?.tools.some((t: any) => t.name === 'update_constitution')).toBe(true);
72 | expect(duration).toBeLessThan(5000);
73 | } finally {
74 | serverProcess.kill();
75 | }
76 | }
77 |
78 | describe('Server Startup and Response Time', () => {
79 | it('should start and respond to a tools/list request over HTTP using MCP_HTTP_PORT', async () => {
80 | await runStartupTest('MCP_HTTP_PORT');
81 | }, 10000);
82 |
83 | it('should start and respond to a tools/list request over HTTP using PORT', async () => {
84 | await runStartupTest('PORT');
85 | }, 10000);
86 |
87 | it('should prefer MCP_HTTP_PORT when both MCP_HTTP_PORT and PORT are set', async () => {
88 | await runStartupTest('BOTH');
89 | }, 10000);
90 | });
91 |
```
--------------------------------------------------------------------------------
/docs/integrations/cpi.md:
--------------------------------------------------------------------------------
```markdown
1 | # CPI Integration
2 |
3 | ## Overview
4 | > CPI (Chain-Pattern Interrupt): a runtime oversight mechanism for multi-agent systems that mitigates “reasoning lock-in.” It injects interrupts based on policy triggers (pattern detectors, heuristics, or external signals), then resumes or reroutes flow.
5 | >
6 | > Core pieces: (1) trigger evaluators, (2) intervention policy (allow/block/route/ask-human), (3) logging & repro harness.
7 | >
8 | > Status: repo includes repro evals; “constitution” tool supports per-session rule-sets.
9 | >
10 | > Integration intent with VibeCheck: VibeCheck = metacognitive layer (signals/traits/uncertainty). CPI = on-policy interrupter. VibeCheck feeds CPI triggers; CPI acts on them.
11 |
12 | CPI composes with VibeCheck by acting as an on-policy interrupter whenever VibeCheck signals a risk spike. Use VibeCheck to surface agent traits, uncertainty, and risk levels, then forward that context to CPI so its policy engine can decide whether to allow, block, reroute, or escalate the next action. The example stub in [`examples/cpi-integration.ts`](../../examples/cpi-integration.ts) illustrates the plumbing you can copy into your own orchestrator.
13 |
14 | ## Flow diagram
15 | ```mermaid
16 | flowchart LR
17 | AgentStep[Agent step] -->|emit signals| VibeCheck
18 | VibeCheck -->|risk + traits| CPI
19 | CPI -->|policy decision| AgentController[Agent controller]
20 | AgentController -->|resume/adjust| AgentStep
21 | ```
22 |
23 | ## Minimal integration sketch
24 | Below is a minimal TypeScript sketch that mirrors the logic in the [`runWithCPI`](../../examples/cpi-integration.ts) example. Replace the TODO markers with the real CPI SDK import when it becomes available.
25 |
26 | ```ts
27 | type AgentStep = {
28 | sessionId: string;
29 | summary: string;
30 | nextAction: string;
31 | };
32 |
33 | type VibeCheckSignal = {
34 | riskScore: number;
35 | advice: string;
36 | };
37 |
38 | async function analyzeWithVibeCheck(step: AgentStep): Promise<VibeCheckSignal> {
39 | // TODO: replace with a real call to the VibeCheck MCP server.
40 | return { riskScore: Math.random(), advice: `Reflect on: ${step.summary}` };
41 | }
42 |
43 | // TODO: replace with `import { createPolicy } from '@cpi/sdk';`
44 | function cpiPolicyShim(signal: VibeCheckSignal) {
45 | if (signal.riskScore >= 0.6) {
46 | return { action: 'interrupt', reason: 'High metacognitive risk from VibeCheck' } as const;
47 | }
48 | return { action: 'allow' } as const;
49 | }
50 |
51 | export async function evaluateStep(step: AgentStep) {
52 | const signal = await analyzeWithVibeCheck(step);
53 | const decision = cpiPolicyShim(signal);
54 |
55 | if (decision.action === 'interrupt') {
56 | // Pause your agent, collect clarification, or reroute to a human.
57 | return { status: 'paused', reason: decision.reason } as const;
58 | }
59 |
60 | return { status: 'continue', signal } as const;
61 | }
62 | ```
63 |
64 | ### Implementation checklist
65 | 1. Surface VibeCheck scores (risk, traits, uncertainty) alongside the raw advice payload.
66 | 2. Normalize those signals into CPI trigger events (e.g., `riskScore > 0.6`).
67 | 3. Hand the event to a CPI intervention policy and respect the returned directive.
68 | 4. Feed decisions into the CPI logging & repro harness to preserve traces.
69 |
70 | ## Further reading
71 | - CPI reference implementation (placeholder): <https://github.com/<ORG>/cpi>
72 | - VibeCheck + CPI wiring example: [`examples/cpi-integration.ts`](../../examples/cpi-integration.ts)
73 |
```
--------------------------------------------------------------------------------
/src/cli/clients/shared.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promises as fsPromises, constants as fsConstants } from 'node:fs';
2 | import { dirname, join, resolve } from 'node:path';
3 | import os from 'node:os';
4 | import { isDeepStrictEqual } from 'node:util';
5 |
6 | const { access, mkdir, readFile, rename, writeFile } = fsPromises;
7 |
8 | export type JsonRecord = Record<string, unknown>;
9 |
10 | export type TransportKind = 'stdio' | 'http';
11 |
12 | export type MergeOpts = {
13 | id: string;
14 | sentinel: string;
15 | transport: TransportKind;
16 | httpUrl?: string;
17 | dev?: {
18 | watch?: boolean;
19 | debug?: string;
20 | };
21 | };
22 |
23 | export type MergeResult = {
24 | next: JsonRecord;
25 | changed: boolean;
26 | reason?: string;
27 | };
28 |
29 | export type ClientDescription = {
30 | name: string;
31 | pathHint: string;
32 | summary?: string;
33 | transports?: TransportKind[];
34 | defaultTransport?: TransportKind;
35 | requiredEnvKeys?: readonly string[];
36 | notes?: string;
37 | docsUrl?: string;
38 | };
39 |
40 | export interface ClientAdapter {
41 | locate(custom?: string): Promise<string | null>;
42 | read(path: string, raw?: string): Promise<JsonRecord>;
43 | merge(config: JsonRecord, entry: JsonRecord, options: MergeOpts): MergeResult;
44 | writeAtomic(path: string, data: JsonRecord): Promise<void>;
45 | describe(): ClientDescription;
46 | }
47 |
48 | export function isRecord(value: unknown): value is JsonRecord {
49 | return typeof value === 'object' && value !== null && !Array.isArray(value);
50 | }
51 |
52 | export async function pathExists(path: string): Promise<boolean> {
53 | try {
54 | await access(path, fsConstants.F_OK);
55 | return true;
56 | } catch {
57 | return false;
58 | }
59 | }
60 |
61 | export function expandHomePath(path: string): string {
62 | if (!path.startsWith('~')) {
63 | return resolve(path);
64 | }
65 |
66 | const home = os.homedir();
67 | if (path === '~') {
68 | return home;
69 | }
70 |
71 | const remainder = path.slice(1);
72 | if (remainder.startsWith('/') || remainder.startsWith('\\')) {
73 | return resolve(join(home, remainder.slice(1)));
74 | }
75 |
76 | return resolve(join(home, remainder));
77 | }
78 |
79 | export async function readJsonFile(path: string, raw?: string, context = 'Client configuration'): Promise<JsonRecord> {
80 | const payload = raw ?? (await readFile(path, 'utf8'));
81 | const parsed = JSON.parse(payload);
82 | if (!isRecord(parsed)) {
83 | throw new Error(`${context} must be a JSON object.`);
84 | }
85 |
86 | return parsed;
87 | }
88 |
89 | export async function writeJsonFileAtomic(path: string, data: JsonRecord): Promise<void> {
90 | await mkdir(dirname(path), { recursive: true });
91 | const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
92 | const payload = `${JSON.stringify(data, null, 2)}\n`;
93 | await writeFile(tempPath, payload, { mode: 0o600 });
94 | await rename(tempPath, path);
95 | }
96 |
97 | export function mergeIntoMap(
98 | config: JsonRecord,
99 | entry: JsonRecord,
100 | options: MergeOpts,
101 | mapKey: string,
102 | ): MergeResult {
103 | const baseConfig = isRecord(config) ? config : {};
104 | const existingMap = isRecord(baseConfig[mapKey]) ? { ...(baseConfig[mapKey] as JsonRecord) } : {};
105 | const currentEntry = isRecord(existingMap[options.id])
106 | ? ({ ...(existingMap[options.id] as JsonRecord) } as JsonRecord)
107 | : null;
108 |
109 | if (currentEntry && currentEntry.managedBy !== options.sentinel) {
110 | return {
111 | next: baseConfig,
112 | changed: false,
113 | reason: `Existing entry "${options.id}" is not managed by ${options.sentinel}.`,
114 | };
115 | }
116 |
117 | const sanitizedEntry = { ...entry } as JsonRecord;
118 | delete sanitizedEntry.managedBy;
119 |
120 | const nextEntry: JsonRecord = { ...sanitizedEntry, managedBy: options.sentinel };
121 | const nextMap = { ...existingMap, [options.id]: nextEntry };
122 | const nextConfig: JsonRecord = { ...baseConfig, [mapKey]: nextMap };
123 |
124 | if (currentEntry && isDeepStrictEqual(currentEntry, nextEntry)) {
125 | return { next: baseConfig, changed: false };
126 | }
127 |
128 | return { next: nextConfig, changed: true };
129 | }
130 |
```
--------------------------------------------------------------------------------
/tests/cursor-merge.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promises as fs } from 'node:fs';
2 | import { readFileSync } from 'node:fs';
3 | import { join } from 'node:path';
4 | import os from 'node:os';
5 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6 | import cursorAdapter from '../src/cli/clients/cursor.js';
7 | import { MergeOpts } from '../src/cli/clients/shared.js';
8 | import { createCliProgram } from '../src/cli/index.js';
9 |
10 | const SENTINEL = 'vibe-check-mcp-cli';
11 | const FIXTURE_DIR = join(process.cwd(), 'tests', 'fixtures', 'cursor');
12 |
13 | function loadFixture(name: string): Record<string, unknown> {
14 | const raw = readFileSync(join(FIXTURE_DIR, name), 'utf8');
15 | return JSON.parse(raw) as Record<string, unknown>;
16 | }
17 |
18 | const ENTRY = {
19 | command: 'npx',
20 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
21 | env: {},
22 | } as const;
23 |
24 | const MERGE_OPTS: MergeOpts = {
25 | id: 'vibe-check-mcp',
26 | sentinel: SENTINEL,
27 | transport: 'stdio',
28 | };
29 |
30 | describe('Cursor MCP config merge', () => {
31 | const ORIGINAL_ENV = { ...process.env };
32 |
33 | beforeEach(() => {
34 | process.exitCode = undefined;
35 | process.env = { ...ORIGINAL_ENV };
36 | });
37 |
38 | afterEach(() => {
39 | vi.restoreAllMocks();
40 | process.env = { ...ORIGINAL_ENV };
41 | });
42 |
43 | it('appends the managed entry to a base config', () => {
44 | const base = loadFixture('config.base.json');
45 | const result = cursorAdapter.merge(base, ENTRY, MERGE_OPTS);
46 | expect(result.changed).toBe(true);
47 | expect(result.next.mcpServers).toMatchObject({
48 | 'vibe-check-mcp': {
49 | command: 'npx',
50 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
51 | env: {},
52 | managedBy: SENTINEL,
53 | },
54 | });
55 | });
56 |
57 | it('updates an existing managed entry in place', () => {
58 | const base = loadFixture('config.with-managed-entry.json');
59 | const result = cursorAdapter.merge(base, ENTRY, MERGE_OPTS);
60 | expect(result.changed).toBe(true);
61 | const next = result.next.mcpServers as Record<string, unknown>;
62 | expect(next['vibe-check-mcp']).toEqual({
63 | command: 'npx',
64 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
65 | env: {},
66 | managedBy: SENTINEL,
67 | });
68 | });
69 |
70 | it('does not replace an unmanaged entry', () => {
71 | const base = loadFixture('../claude/config.with-other-servers.json');
72 | const result = cursorAdapter.merge(base, ENTRY, MERGE_OPTS);
73 | expect(result.changed).toBe(false);
74 | expect(result.reason).toContain('not managed');
75 | });
76 |
77 | it('creates a backup and writes via the CLI install command', async () => {
78 | const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'cursor-merge-'));
79 | const configPath = join(tmpDir, 'mcp.json');
80 | const original = readFileSync(join(FIXTURE_DIR, 'config.base.json'), 'utf8');
81 | await fs.writeFile(configPath, original, 'utf8');
82 |
83 | process.env.OPENAI_API_KEY = 'sk-cursor-key';
84 |
85 | const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
86 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
87 |
88 | const program = createCliProgram();
89 | await program.parseAsync([
90 | 'node',
91 | 'vibe-check-mcp',
92 | 'install',
93 | '--client',
94 | 'cursor',
95 | '--config',
96 | configPath,
97 | '--non-interactive',
98 | ]);
99 |
100 | logSpy.mockRestore();
101 | warnSpy.mockRestore();
102 |
103 | const files = await fs.readdir(tmpDir);
104 | const backup = files.find((file) => file.endsWith('.bak'));
105 | expect(backup).toBeDefined();
106 | if (backup) {
107 | const backupContent = await fs.readFile(join(tmpDir, backup), 'utf8');
108 | expect(backupContent).toBe(original);
109 | }
110 |
111 | const finalContent = await fs.readFile(configPath, 'utf8');
112 | const parsed = JSON.parse(finalContent) as Record<string, any>;
113 | expect(parsed.mcpServers['vibe-check-mcp']).toEqual({
114 | command: 'npx',
115 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
116 | env: {},
117 | managedBy: SENTINEL,
118 | });
119 | });
120 | });
121 |
```
--------------------------------------------------------------------------------
/tests/vscode-merge.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promises as fs } from 'node:fs';
2 | import { readFileSync } from 'node:fs';
3 | import { join } from 'node:path';
4 | import os from 'node:os';
5 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6 | import vscodeAdapter from '../src/cli/clients/vscode.js';
7 | import { MergeOpts } from '../src/cli/clients/shared.js';
8 | import { createCliProgram } from '../src/cli/index.js';
9 |
10 | const SENTINEL = 'vibe-check-mcp-cli';
11 | const FIXTURE_DIR = join(process.cwd(), 'tests', 'fixtures', 'vscode');
12 |
13 | function loadFixture(name: string): Record<string, unknown> {
14 | const raw = readFileSync(join(FIXTURE_DIR, name), 'utf8');
15 | return JSON.parse(raw) as Record<string, unknown>;
16 | }
17 |
18 | describe('VS Code MCP config merge', () => {
19 | const ORIGINAL_ENV = { ...process.env };
20 |
21 | beforeEach(() => {
22 | process.exitCode = undefined;
23 | process.env = { ...ORIGINAL_ENV };
24 | });
25 |
26 | afterEach(() => {
27 | vi.restoreAllMocks();
28 | process.env = { ...ORIGINAL_ENV };
29 | });
30 |
31 | it('appends the managed entry under servers', () => {
32 | const base = loadFixture('workspace.mcp.base.json');
33 | const entry = {
34 | command: 'npx',
35 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
36 | env: {},
37 | } as const;
38 | const options: MergeOpts = {
39 | id: 'vibe-check-mcp',
40 | sentinel: SENTINEL,
41 | transport: 'stdio',
42 | };
43 |
44 | const result = vscodeAdapter.merge(base, entry, options);
45 | expect(result.changed).toBe(true);
46 | const next = result.next.servers as Record<string, any>;
47 | expect(next['vibe-check-mcp']).toEqual({
48 | command: 'npx',
49 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
50 | env: {},
51 | transport: 'stdio',
52 | managedBy: SENTINEL,
53 | });
54 | });
55 |
56 | it('adds dev configuration when requested', () => {
57 | const base = loadFixture('workspace.mcp.base.json');
58 | const entry = {
59 | command: 'npx',
60 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
61 | env: {},
62 | } as const;
63 | const options: MergeOpts = {
64 | id: 'vibe-check-mcp',
65 | sentinel: SENTINEL,
66 | transport: 'stdio',
67 | dev: {
68 | watch: true,
69 | debug: 'node',
70 | },
71 | };
72 |
73 | const result = vscodeAdapter.merge(base, entry, options);
74 | const server = (result.next.servers as Record<string, any>)['vibe-check-mcp'];
75 | expect(server.dev).toEqual({ watch: true, debug: 'node' });
76 | });
77 |
78 | it('creates a backup and writes via CLI install', async () => {
79 | const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'vscode-merge-'));
80 | const configDir = join(tmpDir, '.vscode');
81 | await fs.mkdir(configDir, { recursive: true });
82 | const configPath = join(configDir, 'mcp.json');
83 | const original = readFileSync(join(FIXTURE_DIR, 'workspace.mcp.base.json'), 'utf8');
84 | await fs.writeFile(configPath, original, 'utf8');
85 |
86 | process.env.OPENROUTER_API_KEY = 'sk-or-vscode-key';
87 |
88 | const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
89 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
90 |
91 | const program = createCliProgram();
92 | await program.parseAsync([
93 | 'node',
94 | 'vibe-check-mcp',
95 | 'install',
96 | '--client',
97 | 'vscode',
98 | '--config',
99 | configPath,
100 | '--non-interactive',
101 | '--dev-watch',
102 | '--dev-debug',
103 | 'node',
104 | ]);
105 |
106 | logSpy.mockRestore();
107 | warnSpy.mockRestore();
108 |
109 | const files = await fs.readdir(configDir);
110 | const backup = files.find((file) => file.endsWith('.bak'));
111 | expect(backup).toBeDefined();
112 |
113 | if (backup) {
114 | const backupContent = await fs.readFile(join(configDir, backup), 'utf8');
115 | expect(backupContent).toBe(original);
116 | }
117 |
118 | const finalContent = await fs.readFile(configPath, 'utf8');
119 | const parsed = JSON.parse(finalContent) as Record<string, any>;
120 | expect(parsed.servers['vibe-check-mcp']).toEqual({
121 | command: 'npx',
122 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
123 | env: {},
124 | transport: 'stdio',
125 | dev: {
126 | watch: true,
127 | debug: 'node',
128 | },
129 | managedBy: SENTINEL,
130 | });
131 | });
132 | });
133 |
```
--------------------------------------------------------------------------------
/tests/claude-merge.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promises as fs } from 'node:fs';
2 | import { readFileSync } from 'node:fs';
3 | import { join } from 'node:path';
4 | import os from 'node:os';
5 | import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
6 | import { createCliProgram } from '../src/cli/index.js';
7 | import { mergeMcpEntry } from '../src/cli/clients/claude.js';
8 |
9 | const SENTINEL = 'vibe-check-mcp-cli';
10 | const FIXTURE_DIR = join(process.cwd(), 'tests', 'fixtures', 'claude');
11 |
12 | function loadFixture(name: string): Record<string, unknown> {
13 | const raw = readFileSync(join(FIXTURE_DIR, name), 'utf8');
14 | return JSON.parse(raw) as Record<string, unknown>;
15 | }
16 |
17 | describe('Claude MCP config merge', () => {
18 | const ORIGINAL_ENV = { ...process.env };
19 |
20 | beforeEach(() => {
21 | process.exitCode = undefined;
22 | process.env = { ...ORIGINAL_ENV };
23 | });
24 |
25 | afterEach(() => {
26 | vi.restoreAllMocks();
27 | process.env = { ...ORIGINAL_ENV };
28 | });
29 |
30 | it('appends the managed entry to a base config', () => {
31 | const base = loadFixture('config.base.json');
32 | const entry = {
33 | command: 'npx',
34 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
35 | env: {},
36 | };
37 |
38 | const result = mergeMcpEntry(base, entry, { id: 'vibe-check-mcp', sentinel: SENTINEL });
39 | expect(result.changed).toBe(true);
40 | expect(result.next.mcpServers).toMatchObject({
41 | 'vibe-check-mcp': {
42 | command: 'npx',
43 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
44 | env: {},
45 | managedBy: SENTINEL,
46 | },
47 | });
48 | expect(result.next.mcpServers).toHaveProperty('other-server');
49 | });
50 |
51 | it('updates an existing managed entry in place', () => {
52 | const base = loadFixture('config.with-managed-entry.json');
53 | const entry = {
54 | command: 'npx',
55 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
56 | env: {},
57 | };
58 |
59 | const result = mergeMcpEntry(base, entry, { id: 'vibe-check-mcp', sentinel: SENTINEL });
60 | expect(result.changed).toBe(true);
61 | const nextServers = result.next.mcpServers as Record<string, unknown>;
62 | expect(nextServers['vibe-check-mcp']).toEqual({
63 | command: 'npx',
64 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
65 | env: {},
66 | managedBy: SENTINEL,
67 | });
68 | });
69 |
70 | it('skips unmanaged entries with the same id', () => {
71 | const base = loadFixture('config.with-other-servers.json');
72 | const entry = {
73 | command: 'npx',
74 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
75 | env: {},
76 | };
77 |
78 | const result = mergeMcpEntry(base, entry, { id: 'vibe-check-mcp', sentinel: SENTINEL });
79 | expect(result.changed).toBe(false);
80 | expect(result.reason).toContain('not managed');
81 | expect(result.next).toEqual(base);
82 | });
83 |
84 | it('writes atomically and creates a backup when installing via CLI', async () => {
85 | const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'vibe-claude-'));
86 | const configPath = join(tmpDir, 'claude.json');
87 | const fixturePath = join(FIXTURE_DIR, 'config.base.json');
88 | const original = readFileSync(fixturePath, 'utf8');
89 | await fs.writeFile(configPath, original, 'utf8');
90 |
91 | process.env.ANTHROPIC_API_KEY = 'sk-ant-test-token';
92 |
93 | const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
94 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
95 |
96 | const program = createCliProgram();
97 | await program.parseAsync([
98 | 'node',
99 | 'vibe-check-mcp',
100 | 'install',
101 | '--client',
102 | 'claude',
103 | '--config',
104 | configPath,
105 | '--non-interactive',
106 | ]);
107 |
108 | logSpy.mockRestore();
109 | warnSpy.mockRestore();
110 |
111 | const files = await fs.readdir(tmpDir);
112 | const backup = files.find((file) => file.endsWith('.bak'));
113 | expect(backup).toBeDefined();
114 | const backupContent = await fs.readFile(join(tmpDir, backup as string), 'utf8');
115 | expect(backupContent).toBe(original);
116 |
117 | const finalContent = await fs.readFile(configPath, 'utf8');
118 | const parsed = JSON.parse(finalContent) as Record<string, any>;
119 | expect(parsed.mcpServers['vibe-check-mcp']).toEqual({
120 | command: 'npx',
121 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
122 | env: {},
123 | managedBy: SENTINEL,
124 | });
125 | expect(parsed.mcpServers).toHaveProperty('other-server');
126 | });
127 | });
128 |
```
--------------------------------------------------------------------------------
/docs/clients.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Client Integration Notes
2 |
3 | This document supplements the CLI installers documented in the [README](../README.md). Each section outlines discovery paths, schema nuances, and post-installation tips.
4 |
5 | > Tip: Run `npx @pv-bhat/vibe-check-mcp --list-clients` to see a quick summary of every supported integration.
6 |
7 | For supported providers, secret resolution, and storage guidance, read [API Keys & Secret Management](./api-keys.md). The notes below cover only client-specific requirements and edge cases.
8 |
9 | ## Claude Desktop
10 |
11 | - **Config path**: `claude_desktop_config.json` (auto-detected per platform).
12 | - **Schema**: `mcpServers` map keyed by server ID.
13 | - **Default transport**: stdio (`npx -y @pv-bhat/vibe-check-mcp start --stdio`).
14 | - **API keys**: Requires `ANTHROPIC_API_KEY`.
15 | - **Restart**: Quit and relaunch Claude Desktop after installation.
16 |
17 | ```jsonc
18 | {
19 | "mcpServers": {
20 | "vibe-check-mcp": {
21 | "command": "npx",
22 | "args": ["-y", "@pv-bhat/vibe-check-mcp", "start", "--stdio"],
23 | "env": {},
24 | "managedBy": "vibe-check-mcp-cli"
25 | }
26 | }
27 | }
28 | ```
29 |
30 | Docs: [Claude Desktop MCP](https://docs.anthropic.com/en/docs/claude-desktop/model-context-protocol)
31 |
32 | ## Claude Code
33 |
34 | - **Config path**: `~/.config/Claude/claude_code_config.json` (auto-detected per platform).
35 | - **Schema**: Claude-style `mcpServers` map.
36 | - **Default transport**: stdio (`npx -y @pv-bhat/vibe-check-mcp start --stdio`).
37 | - **API keys**: Requires `ANTHROPIC_API_KEY`.
38 | - **Bootstrap**: Run `claude code login` or any Claude Code command once to scaffold the config file.
39 |
40 | ```jsonc
41 | {
42 | "mcpServers": {
43 | "vibe-check-mcp": {
44 | "command": "npx",
45 | "args": ["-y", "@pv-bhat/vibe-check-mcp", "start", "--stdio"],
46 | "env": {},
47 | "managedBy": "vibe-check-mcp-cli"
48 | }
49 | }
50 | }
51 | ```
52 |
53 | Docs: [Claude Code MCP integration](https://docs.anthropic.com/en/docs/agents/claude-code)
54 |
55 | ## Cursor
56 |
57 | - **Config path**: `~/.cursor/mcp.json` (override with `--config` if needed).
58 | - **Schema**: Claude-style `mcpServers` map.
59 | - **Fallback**: When the config file is missing, the CLI prints a JSON snippet suitable for Cursor's MCP settings UI.
60 |
61 | ```jsonc
62 | {
63 | "mcpServers": {
64 | "vibe-check-mcp": {
65 | "command": "npx",
66 | "args": ["-y", "@pv-bhat/vibe-check-mcp", "start", "--stdio"],
67 | "env": {},
68 | "managedBy": "vibe-check-mcp-cli"
69 | }
70 | }
71 | }
72 | ```
73 |
74 | Reference: [Cursor community thread on MCP](https://forum.cursor.so/t/mcp-support/1487)
75 |
76 | ## Windsurf (Cascade)
77 |
78 | - **Config paths**: legacy `~/.codeium/windsurf/mcp_config.json`, new builds `~/.codeium/mcp_config.json`.
79 | - **Transports**: stdio by default; HTTP uses `serverUrl`.
80 | - **Restart**: Close and reopen Windsurf to reload MCP servers.
81 |
82 | ```jsonc
83 | // stdio
84 | {
85 | "mcpServers": {
86 | "vibe-check-mcp": {
87 | "command": "npx",
88 | "args": ["-y", "@pv-bhat/vibe-check-mcp", "start", "--stdio"],
89 | "env": {},
90 | "managedBy": "vibe-check-mcp-cli"
91 | }
92 | }
93 | }
94 |
95 | // http
96 | {
97 | "mcpServers": {
98 | "vibe-check-mcp": {
99 | "serverUrl": "http://127.0.0.1:2091",
100 | "managedBy": "vibe-check-mcp-cli"
101 | }
102 | }
103 | }
104 | ```
105 |
106 | Docs: [Codeium Windsurf MCP guide](https://docs.codeium.com/windsurf/model-context-protocol)
107 |
108 | ## Visual Studio Code
109 |
110 | - **Workspace config**: `.vscode/mcp.json` (profiles use the VS Code user data dir).
111 | - **Transports**: stdio (`transport: "stdio"`) or HTTP (`url` + `transport: "http"`).
112 | - **CLI tips**: Provide `--config` to target a workspace file. Without it, the CLI prints a JSON snippet plus a `vscode:mcp/install?...` quick-install link.
113 | - **Dev helpers**: `--dev-watch` sets `dev.watch=true`; `--dev-debug <value>` populates `dev.debug`.
114 |
115 | ```jsonc
116 | {
117 | "servers": {
118 | "vibe-check-mcp": {
119 | "command": "npx",
120 | "args": ["-y", "@pv-bhat/vibe-check-mcp", "start", "--stdio"],
121 | "env": {},
122 | "transport": "stdio",
123 | "managedBy": "vibe-check-mcp-cli"
124 | }
125 | }
126 | }
127 | ```
128 |
129 | Docs:
130 | - [VS Code MCP announcement](https://code.visualstudio.com/updates/v1_102#_model-context-protocol)
131 | - [VS Code MCP quickstart](https://code.visualstudio.com/docs/copilot/mcp)
132 |
133 | ## JetBrains (future)
134 |
135 | JetBrains AI Assistant already consumes Claude-style MCP configs over stdio. Import from Claude or point it at the same command once JetBrains exposes MCP import hooks publicly.
136 |
137 | Docs: [JetBrains AI Assistant MCP](https://blog.jetbrains.com/ai/2024/08/model-context-protocol/)
138 |
```
--------------------------------------------------------------------------------
/tests/windsurf-merge.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promises as fs } from 'node:fs';
2 | import { readFileSync } from 'node:fs';
3 | import { join } from 'node:path';
4 | import os from 'node:os';
5 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6 | import windsurfAdapter from '../src/cli/clients/windsurf.js';
7 | import { MergeOpts } from '../src/cli/clients/shared.js';
8 | import { createCliProgram } from '../src/cli/index.js';
9 |
10 | const SENTINEL = 'vibe-check-mcp-cli';
11 | const FIXTURE_DIR = join(process.cwd(), 'tests', 'fixtures', 'windsurf');
12 |
13 | function loadFixture(name: string): Record<string, unknown> {
14 | const raw = readFileSync(join(FIXTURE_DIR, name), 'utf8');
15 | return JSON.parse(raw) as Record<string, unknown>;
16 | }
17 |
18 | describe('Windsurf MCP config merge', () => {
19 | const ORIGINAL_ENV = { ...process.env };
20 |
21 | beforeEach(() => {
22 | process.exitCode = undefined;
23 | process.env = { ...ORIGINAL_ENV };
24 | });
25 |
26 | afterEach(() => {
27 | vi.restoreAllMocks();
28 | process.env = { ...ORIGINAL_ENV };
29 | });
30 |
31 | it('appends a stdio entry to a base config', () => {
32 | const base = loadFixture('config.base.json');
33 | const entry = {
34 | command: 'npx',
35 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
36 | env: {},
37 | } as const;
38 | const options: MergeOpts = {
39 | id: 'vibe-check-mcp',
40 | sentinel: SENTINEL,
41 | transport: 'stdio',
42 | };
43 |
44 | const result = windsurfAdapter.merge(base, entry, options);
45 | expect(result.changed).toBe(true);
46 | const next = result.next.mcpServers as Record<string, unknown>;
47 | expect(next['vibe-check-mcp']).toEqual({
48 | command: 'npx',
49 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
50 | env: {},
51 | managedBy: SENTINEL,
52 | });
53 | });
54 |
55 | it('preserves a managed http entry and updates the URL', () => {
56 | const base = loadFixture('config.with-http-entry.json');
57 | const entry = {
58 | command: 'npx',
59 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--http', '--port', '3000'],
60 | env: {},
61 | } as const;
62 | const options: MergeOpts = {
63 | id: 'vibe-check-mcp',
64 | sentinel: SENTINEL,
65 | transport: 'http',
66 | httpUrl: 'http://127.0.0.1:3000',
67 | };
68 |
69 | const result = windsurfAdapter.merge(base, entry, options);
70 | expect(result.changed).toBe(true);
71 | const next = result.next.mcpServers as Record<string, any>;
72 | expect(next['vibe-check-mcp']).toEqual({
73 | serverUrl: 'http://127.0.0.1:3000',
74 | managedBy: SENTINEL,
75 | });
76 | });
77 |
78 | it('does not replace unmanaged entries', () => {
79 | const base = loadFixture('../claude/config.with-other-servers.json');
80 | const entry = {
81 | command: 'npx',
82 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
83 | env: {},
84 | } as const;
85 | const options: MergeOpts = {
86 | id: 'vibe-check-mcp',
87 | sentinel: SENTINEL,
88 | transport: 'stdio',
89 | };
90 |
91 | const result = windsurfAdapter.merge(base, entry, options);
92 | expect(result.changed).toBe(false);
93 | expect(result.reason).toContain('not managed');
94 | });
95 |
96 | it('creates a backup and writes via CLI install', async () => {
97 | const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'windsurf-merge-'));
98 | const configPath = join(tmpDir, 'mcp_config.json');
99 | const original = readFileSync(join(FIXTURE_DIR, 'config.base.json'), 'utf8');
100 | await fs.writeFile(configPath, original, 'utf8');
101 |
102 | process.env.GEMINI_API_KEY = 'AI-windsurf-key';
103 |
104 | const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
105 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
106 |
107 | const program = createCliProgram();
108 | await program.parseAsync([
109 | 'node',
110 | 'vibe-check-mcp',
111 | 'install',
112 | '--client',
113 | 'windsurf',
114 | '--config',
115 | configPath,
116 | '--non-interactive',
117 | ]);
118 |
119 | logSpy.mockRestore();
120 | warnSpy.mockRestore();
121 |
122 | const files = await fs.readdir(tmpDir);
123 | const backup = files.find((file) => file.endsWith('.bak'));
124 | expect(backup).toBeDefined();
125 |
126 | if (backup) {
127 | const backupContent = await fs.readFile(join(tmpDir, backup), 'utf8');
128 | expect(backupContent).toBe(original);
129 | }
130 |
131 | const finalContent = await fs.readFile(configPath, 'utf8');
132 | const parsed = JSON.parse(finalContent) as Record<string, any>;
133 | expect(parsed.mcpServers['vibe-check-mcp']).toEqual({
134 | command: 'npx',
135 | args: ['-y', '@pv-bhat/vibe-check-mcp', 'start', '--stdio'],
136 | env: {},
137 | managedBy: SENTINEL,
138 | });
139 | });
140 | });
141 |
```