@vibe-assurance/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -0
- package/bin/vibe.js +41 -0
- package/package.json +44 -0
- package/src/api/client.js +132 -0
- package/src/commands/login.js +234 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/mcp-server.js +29 -0
- package/src/commands/setup-claude.js +112 -0
- package/src/config/credentials.js +121 -0
- package/src/mcp/server.js +111 -0
- package/src/mcp/tools.js +271 -0
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @vibe-assurance/cli
|
|
2
|
+
|
|
3
|
+
Connect Claude Code to your Vibe Assurance governance platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @vibe-assurance/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
1. **Login to Vibe Assurance:**
|
|
14
|
+
```bash
|
|
15
|
+
vibe login
|
|
16
|
+
```
|
|
17
|
+
This opens your browser for authentication.
|
|
18
|
+
|
|
19
|
+
2. **Configure Claude Code:**
|
|
20
|
+
```bash
|
|
21
|
+
vibe setup-claude
|
|
22
|
+
```
|
|
23
|
+
This adds the Vibe Assurance MCP server to your Claude Code configuration.
|
|
24
|
+
|
|
25
|
+
3. **Restart Claude Code** and start using governance tools!
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Available Commands
|
|
30
|
+
|
|
31
|
+
| Command | Description |
|
|
32
|
+
|---------|-------------|
|
|
33
|
+
| `vibe login` | Authenticate with Vibe Assurance |
|
|
34
|
+
| `vibe logout` | Clear stored credentials |
|
|
35
|
+
| `vibe mcp-server` | Start the MCP server (used by Claude Code) |
|
|
36
|
+
| `vibe setup-claude` | Configure Claude Code to use Vibe Assurance |
|
|
37
|
+
|
|
38
|
+
### Available Tools in Claude Code
|
|
39
|
+
|
|
40
|
+
Once configured, Claude Code has access to these tools:
|
|
41
|
+
|
|
42
|
+
| Tool | Description |
|
|
43
|
+
|------|-------------|
|
|
44
|
+
| `vibe_get_role` | Get an AI analyst role prompt |
|
|
45
|
+
| `vibe_list_roles` | List available roles |
|
|
46
|
+
| `vibe_get_context` | Get your governance context (CRs, risks, vulns) |
|
|
47
|
+
| `vibe_get_template` | Get a document template |
|
|
48
|
+
| `vibe_list_templates` | List available templates |
|
|
49
|
+
| `vibe_store_artifact` | Store a created document |
|
|
50
|
+
| `vibe_update_artifact` | Update an existing artifact |
|
|
51
|
+
| `vibe_list_artifacts` | List your stored artifacts |
|
|
52
|
+
| `vibe_get_artifact` | Get a specific artifact |
|
|
53
|
+
| `vibe_delete_artifact` | Delete an artifact |
|
|
54
|
+
|
|
55
|
+
### Example Session
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
User: Create a change request for adding user authentication
|
|
59
|
+
|
|
60
|
+
Claude: I'll help you create a change request. Let me get your context first.
|
|
61
|
+
|
|
62
|
+
[Calls vibe_get_context]
|
|
63
|
+
[Calls vibe_get_role("implementation-planner")]
|
|
64
|
+
[Calls vibe_get_template("change-request")]
|
|
65
|
+
|
|
66
|
+
Based on your governance context, your next CR ID is CR-2026-001.
|
|
67
|
+
|
|
68
|
+
[Creates the CR documents...]
|
|
69
|
+
|
|
70
|
+
Done! Would you like me to store this in Vibe Assurance?
|
|
71
|
+
|
|
72
|
+
User: Yes
|
|
73
|
+
|
|
74
|
+
Claude: [Calls vibe_store_artifact(...)]
|
|
75
|
+
|
|
76
|
+
Your CR-2026-001 has been stored! View it at:
|
|
77
|
+
https://vibeassurance.app/governance/artifacts/CR-2026-001
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
### Environment Variables
|
|
83
|
+
|
|
84
|
+
| Variable | Default | Description |
|
|
85
|
+
|----------|---------|-------------|
|
|
86
|
+
| `VIBE_API_URL` | `https://agent-platform-prod.azurewebsites.net` | API base URL |
|
|
87
|
+
|
|
88
|
+
For development, you can point to a local instance:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
VIBE_API_URL=http://localhost:3000 vibe login
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Credential Storage
|
|
95
|
+
|
|
96
|
+
Credentials are stored securely using:
|
|
97
|
+
- **macOS**: Keychain Access
|
|
98
|
+
- **Windows**: Credential Manager
|
|
99
|
+
- **Linux**: libsecret (if available)
|
|
100
|
+
|
|
101
|
+
If the system keychain is unavailable, credentials fall back to `~/.vibe/credentials.json` with restricted permissions (600).
|
|
102
|
+
|
|
103
|
+
## Requirements
|
|
104
|
+
|
|
105
|
+
- **Node.js 18** or later
|
|
106
|
+
- **Vibe Assurance account**
|
|
107
|
+
- **Claude Code**
|
|
108
|
+
|
|
109
|
+
## Troubleshooting
|
|
110
|
+
|
|
111
|
+
### "Not authenticated" error
|
|
112
|
+
|
|
113
|
+
Run `vibe login` to authenticate. If you see this error during MCP server startup, your session may have expired.
|
|
114
|
+
|
|
115
|
+
### MCP server not connecting
|
|
116
|
+
|
|
117
|
+
1. Run `vibe setup-claude` again
|
|
118
|
+
2. Restart Claude Code completely (not just reload)
|
|
119
|
+
3. Check that `vibe` command is in your PATH
|
|
120
|
+
|
|
121
|
+
### "Port 38274 is already in use"
|
|
122
|
+
|
|
123
|
+
Another `vibe login` process is running. Wait for it to complete or terminate it.
|
|
124
|
+
|
|
125
|
+
### Keychain access denied
|
|
126
|
+
|
|
127
|
+
The CLI will automatically fall back to file-based storage. This is secure but less convenient.
|
|
128
|
+
|
|
129
|
+
On Linux, install `libsecret` for keychain support:
|
|
130
|
+
```bash
|
|
131
|
+
# Ubuntu/Debian
|
|
132
|
+
sudo apt-get install libsecret-1-dev
|
|
133
|
+
|
|
134
|
+
# Fedora
|
|
135
|
+
sudo dnf install libsecret-devel
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Debug mode
|
|
139
|
+
|
|
140
|
+
To see detailed logs:
|
|
141
|
+
```bash
|
|
142
|
+
DEBUG=vibe* vibe mcp-server
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Uninstalling
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Remove the CLI
|
|
149
|
+
npm uninstall -g @vibe-assurance/cli
|
|
150
|
+
|
|
151
|
+
# Clear stored credentials
|
|
152
|
+
vibe logout
|
|
153
|
+
|
|
154
|
+
# Or manually:
|
|
155
|
+
rm -rf ~/.vibe/
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Then remove the MCP configuration from Claude Code:
|
|
159
|
+
1. Open `~/.claude/mcp.json` (or your platform's config location)
|
|
160
|
+
2. Remove the `vibeassurance` entry from `mcpServers`
|
|
161
|
+
3. Restart Claude Code
|
|
162
|
+
|
|
163
|
+
## Security
|
|
164
|
+
|
|
165
|
+
- Credentials are stored in the OS keychain when available
|
|
166
|
+
- Tokens are never logged or exposed in console output
|
|
167
|
+
- All API communication uses HTTPS
|
|
168
|
+
- Tokens auto-refresh when expired
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
173
|
+
|
|
174
|
+
## Related
|
|
175
|
+
|
|
176
|
+
- [Vibe Assurance](https://vibeassurance.app) - Governance platform
|
|
177
|
+
- [Claude Code](https://claude.ai/code) - AI coding assistant
|
|
178
|
+
- [MCP Protocol](https://modelcontextprotocol.io) - Model Context Protocol
|
package/bin/vibe.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Load .env file if present (for local development)
|
|
4
|
+
const path = require('path');
|
|
5
|
+
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
|
6
|
+
|
|
7
|
+
const { program } = require('commander');
|
|
8
|
+
const pkg = require('../package.json');
|
|
9
|
+
|
|
10
|
+
// Commands
|
|
11
|
+
const login = require('../src/commands/login');
|
|
12
|
+
const logout = require('../src/commands/logout');
|
|
13
|
+
const mcpServer = require('../src/commands/mcp-server');
|
|
14
|
+
const setupClaude = require('../src/commands/setup-claude');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('vibe')
|
|
18
|
+
.description('Vibe Assurance CLI - Connect Claude Code to your governance platform')
|
|
19
|
+
.version(pkg.version);
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command('login')
|
|
23
|
+
.description('Authenticate with Vibe Assurance')
|
|
24
|
+
.action(login);
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('logout')
|
|
28
|
+
.description('Clear stored credentials')
|
|
29
|
+
.action(logout);
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('mcp-server')
|
|
33
|
+
.description('Start the MCP server for Claude Code')
|
|
34
|
+
.action(mcpServer);
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('setup-claude')
|
|
38
|
+
.description('Configure Claude Code to use the Vibe Assurance MCP server')
|
|
39
|
+
.action(setupClaude);
|
|
40
|
+
|
|
41
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibe-assurance/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vibe Assurance CLI - Connect Claude Code to your governance platform",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibe": "./bin/vibe.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"lint": "eslint src/"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"vibe-assurance",
|
|
15
|
+
"governance",
|
|
16
|
+
"mcp",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"compliance",
|
|
19
|
+
"security"
|
|
20
|
+
],
|
|
21
|
+
"author": "Vibe Assurance",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/vibeassurance/cli.git"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
|
+
"axios": "^1.6.0",
|
|
33
|
+
"chalk": "^4.1.2",
|
|
34
|
+
"commander": "^12.0.0",
|
|
35
|
+
"dotenv": "^16.6.1",
|
|
36
|
+
"keytar": "^7.9.0",
|
|
37
|
+
"open": "^8.4.2",
|
|
38
|
+
"ora": "^5.4.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"eslint": "^8.56.0",
|
|
42
|
+
"jest": "^29.7.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { getCredentials, storeCredentials, deleteCredentials } = require('../config/credentials');
|
|
3
|
+
|
|
4
|
+
// Default to production URL, can be overridden via environment variable
|
|
5
|
+
const API_BASE_URL = process.env.VIBE_API_URL || 'https://agent-platform-prod.azurewebsites.net';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create an authenticated axios instance with token refresh handling
|
|
9
|
+
* @returns {Promise<import('axios').AxiosInstance>}
|
|
10
|
+
*/
|
|
11
|
+
async function createClient() {
|
|
12
|
+
const creds = await getCredentials();
|
|
13
|
+
if (!creds || !creds.accessToken) {
|
|
14
|
+
throw new Error('Not authenticated. Run: vibe login');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const client = axios.create({
|
|
18
|
+
baseURL: API_BASE_URL,
|
|
19
|
+
headers: {
|
|
20
|
+
'Authorization': `Bearer ${creds.accessToken}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'User-Agent': 'vibe-cli/1.0.0'
|
|
23
|
+
},
|
|
24
|
+
timeout: 30000 // 30 second timeout
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Response interceptor for automatic token refresh on 401
|
|
28
|
+
client.interceptors.response.use(
|
|
29
|
+
response => response,
|
|
30
|
+
async error => {
|
|
31
|
+
const originalRequest = error.config;
|
|
32
|
+
|
|
33
|
+
// If 401 and we haven't tried refreshing yet
|
|
34
|
+
if (error.response?.status === 401 && !originalRequest._retry && creds.refreshToken) {
|
|
35
|
+
originalRequest._retry = true;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const newTokens = await refreshToken(creds.refreshToken);
|
|
39
|
+
await storeCredentials(newTokens);
|
|
40
|
+
|
|
41
|
+
// Update the failed request with new token
|
|
42
|
+
originalRequest.headers['Authorization'] = `Bearer ${newTokens.accessToken}`;
|
|
43
|
+
return client(originalRequest);
|
|
44
|
+
} catch (refreshError) {
|
|
45
|
+
// Refresh failed - clear credentials and require re-login
|
|
46
|
+
await deleteCredentials();
|
|
47
|
+
throw new Error('Session expired. Please run: vibe login');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Format error message
|
|
52
|
+
const errorMessage = error.response?.data?.error ||
|
|
53
|
+
error.response?.data?.message ||
|
|
54
|
+
error.message ||
|
|
55
|
+
'Unknown error';
|
|
56
|
+
throw new Error(errorMessage);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return client;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Refresh access token using refresh token
|
|
65
|
+
* @param {string} refreshTokenValue
|
|
66
|
+
* @returns {Promise<Object>} New token object
|
|
67
|
+
*/
|
|
68
|
+
async function refreshToken(refreshTokenValue) {
|
|
69
|
+
const response = await axios.post(`${API_BASE_URL}/api/auth/refresh`, {
|
|
70
|
+
refreshToken: refreshTokenValue
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
accessToken: response.data.token || response.data.accessToken,
|
|
75
|
+
refreshToken: response.data.refreshToken || refreshTokenValue,
|
|
76
|
+
expiresAt: response.data.expiresAt || new Date(Date.now() + 3600 * 1000).toISOString()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Make a GET request to the API
|
|
82
|
+
* @param {string} path - API path (e.g., '/api/mcp/roles')
|
|
83
|
+
* @returns {Promise<any>} Response data
|
|
84
|
+
*/
|
|
85
|
+
async function get(path) {
|
|
86
|
+
const client = await createClient();
|
|
87
|
+
const response = await client.get(path);
|
|
88
|
+
return response.data;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Make a POST request to the API
|
|
93
|
+
* @param {string} path - API path
|
|
94
|
+
* @param {Object} data - Request body
|
|
95
|
+
* @returns {Promise<any>} Response data
|
|
96
|
+
*/
|
|
97
|
+
async function post(path, data) {
|
|
98
|
+
const client = await createClient();
|
|
99
|
+
const response = await client.post(path, data);
|
|
100
|
+
return response.data;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Make a PUT request to the API
|
|
105
|
+
* @param {string} path - API path
|
|
106
|
+
* @param {Object} data - Request body
|
|
107
|
+
* @returns {Promise<any>} Response data
|
|
108
|
+
*/
|
|
109
|
+
async function put(path, data) {
|
|
110
|
+
const client = await createClient();
|
|
111
|
+
const response = await client.put(path, data);
|
|
112
|
+
return response.data;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Make a DELETE request to the API
|
|
117
|
+
* @param {string} path - API path
|
|
118
|
+
* @returns {Promise<any>} Response data
|
|
119
|
+
*/
|
|
120
|
+
async function del(path) {
|
|
121
|
+
const client = await createClient();
|
|
122
|
+
const response = await client.delete(path);
|
|
123
|
+
return response.data;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
get,
|
|
128
|
+
post,
|
|
129
|
+
put,
|
|
130
|
+
delete: del,
|
|
131
|
+
API_BASE_URL
|
|
132
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const open = require('open');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const { URL } = require('url');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const { storeCredentials, hasValidCredentials } = require('../config/credentials');
|
|
7
|
+
const { API_BASE_URL } = require('../api/client');
|
|
8
|
+
|
|
9
|
+
// Local callback server port (chosen to be unlikely to conflict)
|
|
10
|
+
const CALLBACK_PORT = 38274;
|
|
11
|
+
const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Login command - authenticates user via OAuth flow
|
|
15
|
+
*/
|
|
16
|
+
async function login() {
|
|
17
|
+
// Check if already logged in
|
|
18
|
+
if (await hasValidCredentials()) {
|
|
19
|
+
console.log(chalk.yellow('You are already logged in.'));
|
|
20
|
+
console.log('Run `vibe logout` to sign out first.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const spinner = ora('Opening browser for authentication...').start();
|
|
25
|
+
|
|
26
|
+
// Track server for cleanup
|
|
27
|
+
let server = null;
|
|
28
|
+
|
|
29
|
+
// Cleanup function to ensure server is closed
|
|
30
|
+
const cleanup = () => {
|
|
31
|
+
if (server) {
|
|
32
|
+
try {
|
|
33
|
+
server.close();
|
|
34
|
+
server = null;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Ignore close errors
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Handle process termination signals
|
|
42
|
+
process.on('SIGINT', () => {
|
|
43
|
+
cleanup();
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
process.on('SIGTERM', () => {
|
|
47
|
+
cleanup();
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Create a promise that will resolve when auth completes or reject on error/timeout
|
|
52
|
+
const authPromise = new Promise((resolve, reject) => {
|
|
53
|
+
// Create local server to receive OAuth callback
|
|
54
|
+
server = http.createServer(async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
57
|
+
|
|
58
|
+
if (url.pathname === '/callback') {
|
|
59
|
+
const accessToken = url.searchParams.get('access_token');
|
|
60
|
+
const refreshToken = url.searchParams.get('refresh_token');
|
|
61
|
+
const expiresIn = url.searchParams.get('expires_in');
|
|
62
|
+
const error = url.searchParams.get('error');
|
|
63
|
+
|
|
64
|
+
if (error) {
|
|
65
|
+
// Authentication failed
|
|
66
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
67
|
+
res.end(getErrorHtml(error));
|
|
68
|
+
server.close();
|
|
69
|
+
reject(new Error(error));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (accessToken) {
|
|
74
|
+
// Calculate expiry time
|
|
75
|
+
const expiresAt = expiresIn
|
|
76
|
+
? new Date(Date.now() + parseInt(expiresIn) * 1000)
|
|
77
|
+
: new Date(Date.now() + 3600 * 1000); // Default 1 hour
|
|
78
|
+
|
|
79
|
+
// Store credentials securely
|
|
80
|
+
await storeCredentials({
|
|
81
|
+
accessToken,
|
|
82
|
+
refreshToken,
|
|
83
|
+
expiresAt: expiresAt.toISOString()
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Send success response to browser
|
|
87
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
88
|
+
res.end(getSuccessHtml());
|
|
89
|
+
|
|
90
|
+
server.close();
|
|
91
|
+
resolve();
|
|
92
|
+
} else {
|
|
93
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
94
|
+
res.end(getErrorHtml('No access token received'));
|
|
95
|
+
server.close();
|
|
96
|
+
reject(new Error('No access token received'));
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// Ignore other requests
|
|
100
|
+
res.writeHead(404);
|
|
101
|
+
res.end();
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
res.writeHead(500);
|
|
105
|
+
res.end();
|
|
106
|
+
server.close();
|
|
107
|
+
reject(err);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Handle server errors
|
|
112
|
+
server.on('error', (err) => {
|
|
113
|
+
if (err.code === 'EADDRINUSE') {
|
|
114
|
+
reject(new Error(`Port ${CALLBACK_PORT} is already in use. Please close any other vibe login attempts.`));
|
|
115
|
+
} else {
|
|
116
|
+
reject(err);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Start listening and open browser
|
|
121
|
+
server.listen(CALLBACK_PORT, async () => {
|
|
122
|
+
try {
|
|
123
|
+
const authUrl = `${API_BASE_URL}/api/auth/cli?callback=${encodeURIComponent(CALLBACK_URL)}`;
|
|
124
|
+
await open(authUrl);
|
|
125
|
+
spinner.text = 'Waiting for authentication in browser...';
|
|
126
|
+
} catch (err) {
|
|
127
|
+
server.close();
|
|
128
|
+
reject(new Error(`Failed to open browser: ${err.message}`));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Timeout after 5 minutes
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
server.close();
|
|
135
|
+
reject(new Error('Authentication timed out after 5 minutes'));
|
|
136
|
+
}, 5 * 60 * 1000);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await authPromise;
|
|
141
|
+
cleanup();
|
|
142
|
+
spinner.succeed('Successfully authenticated!');
|
|
143
|
+
console.log(chalk.green('\nYou are now logged in to Vibe Assurance.'));
|
|
144
|
+
console.log('\nNext steps:');
|
|
145
|
+
console.log(' 1. Run `vibe setup-claude` to configure Claude Code');
|
|
146
|
+
console.log(' 2. Restart Claude Code');
|
|
147
|
+
console.log(' 3. Start using vibe_* tools in your conversations');
|
|
148
|
+
} catch (err) {
|
|
149
|
+
cleanup();
|
|
150
|
+
spinner.fail(`Authentication failed: ${err.message}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate success HTML page
|
|
157
|
+
* Matches the main app's branding with Tailwind CSS and emerald colors
|
|
158
|
+
*/
|
|
159
|
+
function getSuccessHtml() {
|
|
160
|
+
return `
|
|
161
|
+
<!DOCTYPE html>
|
|
162
|
+
<html lang="en">
|
|
163
|
+
<head>
|
|
164
|
+
<meta charset="UTF-8">
|
|
165
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
166
|
+
<title>Login Successful | Vibe Assurance</title>
|
|
167
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
168
|
+
<script>tailwind.config={darkMode:'class',theme:{extend:{colors:{brand:{500:'#10b981',600:'#059669'}}}}}</script>
|
|
169
|
+
<script>(function(){var d=document.documentElement;var t=localStorage.getItem('theme');if(t==='dark'||(!t&&matchMedia('(prefers-color-scheme:dark)').matches))d.classList.add('dark')})();</script>
|
|
170
|
+
</head>
|
|
171
|
+
<body class="min-h-screen flex flex-col items-center justify-center p-4 bg-zinc-50 dark:bg-zinc-950 transition-colors">
|
|
172
|
+
<div class="absolute inset-0 pointer-events-none opacity-40 dark:opacity-10 bg-[radial-gradient(#cbd5e1_1px,transparent_1px)] [background-size:16px_16px]"></div>
|
|
173
|
+
|
|
174
|
+
<div class="mb-8 text-center relative z-10">
|
|
175
|
+
<div class="flex items-center justify-center gap-3 text-2xl font-bold text-zinc-900 dark:text-white tracking-tight">
|
|
176
|
+
<div class="w-10 h-10 bg-emerald-600 rounded-lg flex items-center justify-center text-white text-xl shadow-sm">V</div>
|
|
177
|
+
Vibe Assurance
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="max-w-md w-full bg-white dark:bg-zinc-900 p-8 rounded-xl shadow-xl border border-zinc-200 dark:border-zinc-800 text-center relative z-10">
|
|
182
|
+
<div class="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
183
|
+
<svg class="w-8 h-8 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
184
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
185
|
+
</svg>
|
|
186
|
+
</div>
|
|
187
|
+
<h1 class="text-xl font-bold text-zinc-900 dark:text-white mb-2">Login Successful!</h1>
|
|
188
|
+
<p class="text-zinc-500 dark:text-zinc-400 text-sm">You can close this window and return to your terminal.</p>
|
|
189
|
+
</div>
|
|
190
|
+
</body>
|
|
191
|
+
</html>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Generate error HTML page
|
|
196
|
+
* Matches the main app's branding with Tailwind CSS
|
|
197
|
+
*/
|
|
198
|
+
function getErrorHtml(error) {
|
|
199
|
+
return `
|
|
200
|
+
<!DOCTYPE html>
|
|
201
|
+
<html lang="en">
|
|
202
|
+
<head>
|
|
203
|
+
<meta charset="UTF-8">
|
|
204
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
205
|
+
<title>Login Failed | Vibe Assurance</title>
|
|
206
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
207
|
+
<script>tailwind.config={darkMode:'class',theme:{extend:{colors:{brand:{500:'#10b981',600:'#059669'}}}}}</script>
|
|
208
|
+
<script>(function(){var d=document.documentElement;var t=localStorage.getItem('theme');if(t==='dark'||(!t&&matchMedia('(prefers-color-scheme:dark)').matches))d.classList.add('dark')})();</script>
|
|
209
|
+
</head>
|
|
210
|
+
<body class="min-h-screen flex flex-col items-center justify-center p-4 bg-zinc-50 dark:bg-zinc-950 transition-colors">
|
|
211
|
+
<div class="absolute inset-0 pointer-events-none opacity-40 dark:opacity-10 bg-[radial-gradient(#cbd5e1_1px,transparent_1px)] [background-size:16px_16px]"></div>
|
|
212
|
+
|
|
213
|
+
<div class="mb-8 text-center relative z-10">
|
|
214
|
+
<div class="flex items-center justify-center gap-3 text-2xl font-bold text-zinc-900 dark:text-white tracking-tight">
|
|
215
|
+
<div class="w-10 h-10 bg-emerald-600 rounded-lg flex items-center justify-center text-white text-xl shadow-sm">V</div>
|
|
216
|
+
Vibe Assurance
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div class="max-w-md w-full bg-white dark:bg-zinc-900 p-8 rounded-xl shadow-xl border border-zinc-200 dark:border-zinc-800 text-center relative z-10">
|
|
221
|
+
<div class="w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
222
|
+
<svg class="w-8 h-8 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
223
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
224
|
+
</svg>
|
|
225
|
+
</div>
|
|
226
|
+
<h1 class="text-xl font-bold text-zinc-900 dark:text-white mb-2">Login Failed</h1>
|
|
227
|
+
<p class="text-red-600 dark:text-red-400 text-sm mb-2">${error}</p>
|
|
228
|
+
<p class="text-zinc-500 dark:text-zinc-400 text-sm">Please close this window and try again.</p>
|
|
229
|
+
</div>
|
|
230
|
+
</body>
|
|
231
|
+
</html>`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = login;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { deleteCredentials, hasValidCredentials } = require('../config/credentials');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Logout command - clears stored credentials
|
|
6
|
+
*/
|
|
7
|
+
async function logout() {
|
|
8
|
+
const wasLoggedIn = await hasValidCredentials();
|
|
9
|
+
|
|
10
|
+
// Delete credentials from all storage locations
|
|
11
|
+
await deleteCredentials();
|
|
12
|
+
|
|
13
|
+
if (wasLoggedIn) {
|
|
14
|
+
console.log(chalk.green('Successfully logged out.'));
|
|
15
|
+
console.log('\nYour local credentials have been cleared.');
|
|
16
|
+
console.log('Your data remains safe in your Vibe Assurance account.');
|
|
17
|
+
} else {
|
|
18
|
+
console.log(chalk.yellow('You were not logged in.'));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = logout;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { hasValidCredentials } = require('../config/credentials');
|
|
3
|
+
const startServer = require('../mcp/server');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MCP Server command
|
|
7
|
+
*
|
|
8
|
+
* Starts the MCP server that Claude Code connects to.
|
|
9
|
+
* Requires authentication (run `vibe login` first).
|
|
10
|
+
*/
|
|
11
|
+
async function mcpServerCommand() {
|
|
12
|
+
// Check if user is authenticated
|
|
13
|
+
if (!(await hasValidCredentials())) {
|
|
14
|
+
// Use stderr for messages (stdout is for MCP protocol)
|
|
15
|
+
console.error(chalk.red('Not authenticated.'));
|
|
16
|
+
console.error('Please run: vibe login');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Start the MCP server
|
|
22
|
+
await startServer();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(chalk.red('Failed to start MCP server:'), error.message);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = mcpServerCommand;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Possible locations for Claude Code MCP configuration
|
|
8
|
+
* Ordered by preference (first existing one will be used)
|
|
9
|
+
*/
|
|
10
|
+
const MCP_CONFIG_PATHS = [
|
|
11
|
+
// Standard location
|
|
12
|
+
path.join(os.homedir(), '.claude', 'mcp.json'),
|
|
13
|
+
// macOS Application Support
|
|
14
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'mcp.json'),
|
|
15
|
+
// Windows AppData
|
|
16
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'mcp.json'),
|
|
17
|
+
// Linux config
|
|
18
|
+
path.join(os.homedir(), '.config', 'claude', 'mcp.json')
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* MCP server configuration for Vibe Assurance
|
|
23
|
+
*/
|
|
24
|
+
const VIBE_MCP_CONFIG = {
|
|
25
|
+
vibeassurance: {
|
|
26
|
+
command: 'vibe',
|
|
27
|
+
args: ['mcp-server']
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Setup Claude command
|
|
33
|
+
*
|
|
34
|
+
* Configures Claude Code to use the Vibe Assurance MCP server
|
|
35
|
+
* by adding the vibeassurance entry to mcp.json
|
|
36
|
+
*/
|
|
37
|
+
async function setupClaude() {
|
|
38
|
+
console.log(chalk.blue('Configuring Claude Code to use Vibe Assurance MCP server...\n'));
|
|
39
|
+
|
|
40
|
+
// Find existing config file or determine default path
|
|
41
|
+
let configPath = null;
|
|
42
|
+
let existingConfig = {};
|
|
43
|
+
|
|
44
|
+
for (const p of MCP_CONFIG_PATHS) {
|
|
45
|
+
if (fs.existsSync(p)) {
|
|
46
|
+
configPath = p;
|
|
47
|
+
try {
|
|
48
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
49
|
+
existingConfig = JSON.parse(content);
|
|
50
|
+
console.log(chalk.gray(`Found existing config at: ${p}`));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.log(chalk.yellow(`Found config file but couldn't parse it, will overwrite: ${p}`));
|
|
53
|
+
existingConfig = {};
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If no existing config found, use the default path
|
|
60
|
+
if (!configPath) {
|
|
61
|
+
configPath = MCP_CONFIG_PATHS[0];
|
|
62
|
+
console.log(chalk.gray(`Creating new config at: ${configPath}`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if vibeassurance is already configured
|
|
66
|
+
if (existingConfig.mcpServers?.vibeassurance) {
|
|
67
|
+
console.log(chalk.yellow('Vibe Assurance is already configured in Claude Code.'));
|
|
68
|
+
console.log('Updating configuration...');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Merge configuration
|
|
72
|
+
const newConfig = {
|
|
73
|
+
...existingConfig,
|
|
74
|
+
mcpServers: {
|
|
75
|
+
...(existingConfig.mcpServers || {}),
|
|
76
|
+
...VIBE_MCP_CONFIG
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Ensure directory exists
|
|
81
|
+
const configDir = path.dirname(configPath);
|
|
82
|
+
if (!fs.existsSync(configDir)) {
|
|
83
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
84
|
+
console.log(chalk.gray(`Created directory: ${configDir}`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Write configuration
|
|
88
|
+
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
|
|
89
|
+
|
|
90
|
+
// Success output
|
|
91
|
+
console.log();
|
|
92
|
+
console.log(chalk.green('Success!') + ' Claude Code has been configured.\n');
|
|
93
|
+
|
|
94
|
+
console.log('Configuration written to:');
|
|
95
|
+
console.log(chalk.gray(` ${configPath}\n`));
|
|
96
|
+
|
|
97
|
+
console.log('The following MCP server was added:');
|
|
98
|
+
console.log(chalk.gray(' vibeassurance: vibe mcp-server\n'));
|
|
99
|
+
|
|
100
|
+
console.log(chalk.blue('Next steps:'));
|
|
101
|
+
console.log(' 1. Restart Claude Code');
|
|
102
|
+
console.log(' 2. The vibe_* tools will be available automatically');
|
|
103
|
+
console.log(' 3. Try asking: "Get my governance context"\n');
|
|
104
|
+
|
|
105
|
+
console.log(chalk.gray('Available tools after restart:'));
|
|
106
|
+
console.log(chalk.gray(' - vibe_get_role : Get AI analyst role prompts'));
|
|
107
|
+
console.log(chalk.gray(' - vibe_get_context : Get CRs, risks, vulnerabilities'));
|
|
108
|
+
console.log(chalk.gray(' - vibe_store_artifact: Store created documents'));
|
|
109
|
+
console.log(chalk.gray(' - ...and more\n'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = setupClaude;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const keytar = require('keytar');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
const SERVICE_NAME = 'vibe-assurance';
|
|
7
|
+
const ACCOUNT_NAME = 'auth-token';
|
|
8
|
+
const FALLBACK_FILE = path.join(os.homedir(), '.vibe', 'credentials.json');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Store authentication token
|
|
12
|
+
* Uses OS keychain (preferred) with file fallback
|
|
13
|
+
* @param {Object} credentials - { accessToken, refreshToken, expiresAt }
|
|
14
|
+
* @returns {Promise<{method: string}>} Storage method used
|
|
15
|
+
*/
|
|
16
|
+
async function storeCredentials(credentials) {
|
|
17
|
+
try {
|
|
18
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));
|
|
19
|
+
return { method: 'keychain' };
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// Keychain unavailable (common on Linux without libsecret, or in containers)
|
|
22
|
+
console.warn('Keychain unavailable, using file storage');
|
|
23
|
+
const dir = path.dirname(FALLBACK_FILE);
|
|
24
|
+
if (!fs.existsSync(dir)) {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
26
|
+
}
|
|
27
|
+
fs.writeFileSync(FALLBACK_FILE, JSON.stringify(credentials), { mode: 0o600 });
|
|
28
|
+
return { method: 'file' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Retrieve stored credentials
|
|
34
|
+
* @returns {Promise<Object|null>} Credentials object or null if not found
|
|
35
|
+
*/
|
|
36
|
+
async function getCredentials() {
|
|
37
|
+
// Try keychain first
|
|
38
|
+
try {
|
|
39
|
+
const stored = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
40
|
+
if (stored) {
|
|
41
|
+
return JSON.parse(stored);
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// Keychain unavailable, try file fallback
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Try file fallback
|
|
48
|
+
if (fs.existsSync(FALLBACK_FILE)) {
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(FALLBACK_FILE, 'utf8');
|
|
51
|
+
return JSON.parse(content);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// File corrupted, return null
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Delete stored credentials from all storage locations
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
async function deleteCredentials() {
|
|
66
|
+
// Delete from keychain
|
|
67
|
+
try {
|
|
68
|
+
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Ignore keychain errors
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Delete file fallback
|
|
74
|
+
if (fs.existsSync(FALLBACK_FILE)) {
|
|
75
|
+
fs.unlinkSync(FALLBACK_FILE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Clean up empty .vibe directory
|
|
79
|
+
const vibeDir = path.dirname(FALLBACK_FILE);
|
|
80
|
+
if (fs.existsSync(vibeDir)) {
|
|
81
|
+
try {
|
|
82
|
+
const files = fs.readdirSync(vibeDir);
|
|
83
|
+
if (files.length === 0) {
|
|
84
|
+
fs.rmdirSync(vibeDir);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Ignore cleanup errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if credentials exist and are valid (not expired)
|
|
94
|
+
* @returns {Promise<boolean>}
|
|
95
|
+
*/
|
|
96
|
+
async function hasValidCredentials() {
|
|
97
|
+
const creds = await getCredentials();
|
|
98
|
+
if (!creds || !creds.accessToken) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if token is expired (with 5 minute buffer)
|
|
103
|
+
if (creds.expiresAt) {
|
|
104
|
+
const expiresAt = new Date(creds.expiresAt);
|
|
105
|
+
const now = new Date();
|
|
106
|
+
const bufferMs = 5 * 60 * 1000; // 5 minutes
|
|
107
|
+
|
|
108
|
+
if (expiresAt.getTime() - bufferMs < now.getTime()) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
storeCredentials,
|
|
118
|
+
getCredentials,
|
|
119
|
+
deleteCredentials,
|
|
120
|
+
hasValidCredentials
|
|
121
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
2
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
3
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
4
|
+
const tools = require('./tools');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Start the MCP server
|
|
8
|
+
*
|
|
9
|
+
* The server communicates with Claude Code via stdio transport,
|
|
10
|
+
* exposing the vibe_* tools for governance operations.
|
|
11
|
+
*/
|
|
12
|
+
async function startServer() {
|
|
13
|
+
// Create MCP server instance using the low-level API
|
|
14
|
+
const server = new Server(
|
|
15
|
+
{
|
|
16
|
+
name: 'vibe-assurance',
|
|
17
|
+
version: '1.0.0'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
capabilities: {
|
|
21
|
+
tools: {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Build tool definitions with enhanced descriptions
|
|
27
|
+
const toolDefinitions = tools.map(tool => {
|
|
28
|
+
let description = tool.description;
|
|
29
|
+
|
|
30
|
+
// Add parameter info to description for tools with parameters
|
|
31
|
+
if (tool.inputSchema?.properties && Object.keys(tool.inputSchema.properties).length > 0) {
|
|
32
|
+
const params = Object.entries(tool.inputSchema.properties)
|
|
33
|
+
.map(([name, schema]) => ` - ${name}: ${schema.description || schema.type}`)
|
|
34
|
+
.join('\n');
|
|
35
|
+
description += `\n\nParameters:\n${params}`;
|
|
36
|
+
if (tool.inputSchema.required?.length > 0) {
|
|
37
|
+
description += `\n\nRequired: ${tool.inputSchema.required.join(', ')}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: tool.name,
|
|
43
|
+
description,
|
|
44
|
+
inputSchema: tool.inputSchema
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Register handler for listing available tools
|
|
49
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
50
|
+
return { tools: toolDefinitions };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Register handler for tool execution
|
|
54
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
55
|
+
const { name, arguments: args } = request.params;
|
|
56
|
+
|
|
57
|
+
// Find the tool by name
|
|
58
|
+
const tool = tools.find(t => t.name === name);
|
|
59
|
+
if (!tool) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: JSON.stringify({
|
|
64
|
+
error: `Unknown tool: ${name}`,
|
|
65
|
+
availableTools: tools.map(t => t.name)
|
|
66
|
+
}, null, 2)
|
|
67
|
+
}],
|
|
68
|
+
isError: true
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Execute the tool handler with the arguments
|
|
74
|
+
const result = await tool.handler(args || {});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: JSON.stringify(result, null, 2)
|
|
80
|
+
}]
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Extract meaningful error message
|
|
84
|
+
const message = error.response?.data?.error ||
|
|
85
|
+
error.response?.data?.message ||
|
|
86
|
+
error.message ||
|
|
87
|
+
'Unknown error occurred';
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: JSON.stringify({
|
|
93
|
+
error: message,
|
|
94
|
+
tool: name
|
|
95
|
+
}, null, 2)
|
|
96
|
+
}],
|
|
97
|
+
isError: true
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Connect via stdio transport (used by Claude Code)
|
|
103
|
+
const transport = new StdioServerTransport();
|
|
104
|
+
await server.connect(transport);
|
|
105
|
+
|
|
106
|
+
// Log to stderr (stdout is used for MCP communication)
|
|
107
|
+
console.error('[vibe-assurance] MCP server started');
|
|
108
|
+
console.error('[vibe-assurance] Available tools:', tools.map(t => t.name).join(', '));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = startServer;
|
package/src/mcp/tools.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const api = require('../api/client');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Tool definitions for Vibe Assurance
|
|
5
|
+
*
|
|
6
|
+
* These tools are exposed to Claude Code via the MCP protocol,
|
|
7
|
+
* allowing Claude to interact with the Vibe Assurance governance platform.
|
|
8
|
+
*/
|
|
9
|
+
const tools = [
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// PULL TOOLS - Get data from Vibe Assurance
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
name: 'vibe_get_role',
|
|
16
|
+
description: 'Get an AI analyst role prompt from Vibe Assurance. Use this to get detailed instructions for roles like implementation-planner, security-auditor, risk-auditor, etc. The role prompt contains the full system prompt and instructions for acting as that role.',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
role: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Role ID (e.g., "implementation-planner", "security-auditor", "risk-auditor", "commit-officer", "test-engineer")'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
required: ['role']
|
|
26
|
+
},
|
|
27
|
+
handler: async ({ role }) => {
|
|
28
|
+
const response = await api.get(`/api/mcp/roles/${role}`);
|
|
29
|
+
return {
|
|
30
|
+
role: response.roleId,
|
|
31
|
+
name: response.name,
|
|
32
|
+
description: response.description,
|
|
33
|
+
prompt: response.systemPrompt,
|
|
34
|
+
referencedTemplates: response.referencedTemplates || [],
|
|
35
|
+
expectedOutputs: response.expectedOutputs || []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
name: 'vibe_list_roles',
|
|
42
|
+
description: 'List all available AI analyst roles in Vibe Assurance. Returns role IDs, names, and descriptions for all governance roles you can use.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {},
|
|
46
|
+
additionalProperties: false
|
|
47
|
+
},
|
|
48
|
+
handler: async () => {
|
|
49
|
+
return await api.get('/api/mcp/roles');
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
name: 'vibe_get_context',
|
|
55
|
+
description: 'Get your current governance context including existing CRs, open risks, vulnerabilities, and the next available CR ID. Use this before creating new CRs to ensure proper ID sequencing.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {},
|
|
59
|
+
additionalProperties: false
|
|
60
|
+
},
|
|
61
|
+
handler: async () => {
|
|
62
|
+
return await api.get('/api/mcp/context');
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
name: 'vibe_get_template',
|
|
68
|
+
description: 'Get a document template from Vibe Assurance. Templates include placeholders and structure for governance documents like change requests, risk assessments, and security reports.',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
template: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Template ID (e.g., "change-request", "risk-assessment", "security-report", "implementation-plan")'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
required: ['template']
|
|
78
|
+
},
|
|
79
|
+
handler: async ({ template }) => {
|
|
80
|
+
return await api.get(`/api/mcp/templates/${template}`);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
name: 'vibe_list_templates',
|
|
86
|
+
description: 'List all available document templates. Returns template IDs, names, descriptions, and categories.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {},
|
|
90
|
+
additionalProperties: false
|
|
91
|
+
},
|
|
92
|
+
handler: async () => {
|
|
93
|
+
return await api.get('/api/mcp/templates');
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// PUSH TOOLS - Store data to Vibe Assurance
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
name: 'vibe_store_artifact',
|
|
103
|
+
description: 'Store a created document (CR, report, risk, vulnerability, etc.) in Vibe Assurance. The artifact will be saved to your account and visible in the web portal.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
type: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
enum: ['CR', 'RISK', 'VULNERABILITY', 'REPORT', 'POLICY'],
|
|
110
|
+
description: 'Type of artifact'
|
|
111
|
+
},
|
|
112
|
+
artifactId: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: 'Unique ID for this artifact (e.g., "CR-2025-024", "VUL-059", "RISK-001")'
|
|
115
|
+
},
|
|
116
|
+
title: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'Title of the artifact'
|
|
119
|
+
},
|
|
120
|
+
status: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
enum: ['Draft', 'Active', 'Completed', 'Closed'],
|
|
123
|
+
description: 'Status of the artifact (default: Draft)'
|
|
124
|
+
},
|
|
125
|
+
content: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'Main content in markdown format'
|
|
128
|
+
},
|
|
129
|
+
files: {
|
|
130
|
+
type: 'array',
|
|
131
|
+
description: 'Additional files for this artifact (e.g., implementation-plan.md, rollback-plan.md for CRs)',
|
|
132
|
+
items: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
name: { type: 'string', description: 'Filename' },
|
|
136
|
+
content: { type: 'string', description: 'File content' }
|
|
137
|
+
},
|
|
138
|
+
required: ['name', 'content']
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
metadata: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
description: 'Additional metadata (e.g., severity, priority, category)',
|
|
144
|
+
additionalProperties: true
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
required: ['type', 'artifactId', 'title', 'content']
|
|
148
|
+
},
|
|
149
|
+
handler: async (params) => {
|
|
150
|
+
return await api.post('/api/mcp/artifacts', params);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
name: 'vibe_update_artifact',
|
|
156
|
+
description: 'Update an existing artifact. You can update the status, content, title, or metadata.',
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
artifactId: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'The artifact ID to update (e.g., "CR-2025-024")'
|
|
163
|
+
},
|
|
164
|
+
status: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
enum: ['Draft', 'Active', 'Completed', 'Closed'],
|
|
167
|
+
description: 'New status'
|
|
168
|
+
},
|
|
169
|
+
content: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'Updated content'
|
|
172
|
+
},
|
|
173
|
+
title: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Updated title'
|
|
176
|
+
},
|
|
177
|
+
files: {
|
|
178
|
+
type: 'array',
|
|
179
|
+
description: 'Updated files array',
|
|
180
|
+
items: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
name: { type: 'string' },
|
|
184
|
+
content: { type: 'string' }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
metadata: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
description: 'Updated metadata'
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
required: ['artifactId']
|
|
194
|
+
},
|
|
195
|
+
handler: async ({ artifactId, ...updates }) => {
|
|
196
|
+
return await api.put(`/api/mcp/artifacts/${artifactId}`, updates);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
{
|
|
201
|
+
name: 'vibe_list_artifacts',
|
|
202
|
+
description: 'List your stored artifacts with optional filters. Use this to see what governance documents you have stored.',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
type: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
enum: ['CR', 'RISK', 'VULNERABILITY', 'REPORT', 'POLICY'],
|
|
209
|
+
description: 'Filter by artifact type'
|
|
210
|
+
},
|
|
211
|
+
status: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
enum: ['Draft', 'Active', 'Completed', 'Closed'],
|
|
214
|
+
description: 'Filter by status'
|
|
215
|
+
},
|
|
216
|
+
limit: {
|
|
217
|
+
type: 'number',
|
|
218
|
+
description: 'Maximum number of results (default: 50, max: 100)'
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
handler: async (params = {}) => {
|
|
223
|
+
const query = new URLSearchParams();
|
|
224
|
+
if (params.type) query.set('type', params.type);
|
|
225
|
+
if (params.status) query.set('status', params.status);
|
|
226
|
+
if (params.limit) query.set('limit', String(Math.min(params.limit, 100)));
|
|
227
|
+
|
|
228
|
+
const queryString = query.toString();
|
|
229
|
+
const path = queryString ? `/api/mcp/artifacts?${queryString}` : '/api/mcp/artifacts';
|
|
230
|
+
return await api.get(path);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
{
|
|
235
|
+
name: 'vibe_get_artifact',
|
|
236
|
+
description: 'Get a specific artifact by ID. Returns the full content including any additional files.',
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
artifactId: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'The artifact ID to retrieve (e.g., "CR-2025-024")'
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
required: ['artifactId']
|
|
246
|
+
},
|
|
247
|
+
handler: async ({ artifactId }) => {
|
|
248
|
+
return await api.get(`/api/mcp/artifacts/${artifactId}`);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
{
|
|
253
|
+
name: 'vibe_delete_artifact',
|
|
254
|
+
description: 'Delete an artifact by ID. This is permanent and cannot be undone.',
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: {
|
|
258
|
+
artifactId: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
description: 'The artifact ID to delete (e.g., "CR-2025-024")'
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
required: ['artifactId']
|
|
264
|
+
},
|
|
265
|
+
handler: async ({ artifactId }) => {
|
|
266
|
+
return await api.delete(`/api/mcp/artifacts/${artifactId}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
module.exports = tools;
|