Skip to content

Commit 9ad37f3

Browse files
kobzevvvclaude
andcommitted
Add vault_add MCP tool — secure credential entry via browser popup
Credentials are entered in a browser form (localhost:9900/add) and go directly to the encrypted store. The password never passes through the LLM's context window. The tool opens the browser, waits for form submit, and returns only { status: "success", site_id } to the agent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ce7ead9 commit 9ad37f3

File tree

5 files changed

+418
-89
lines changed

5 files changed

+418
-89
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"dist"
1212
],
1313
"scripts": {
14-
"build": "tsc && cp src/dashboard/index.html dist/dashboard/",
14+
"build": "tsc && cp src/dashboard/*.html dist/dashboard/",
1515
"dev": "tsc --watch",
1616
"start": "node dist/index.js",
1717
"serve": "node dist/index.js serve",

src/dashboard/add.html

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Vault MCP — Add Credential</title>
7+
<style>
8+
:root {
9+
--bg: #0d1117; --surface: #161b22; --border: #30363d;
10+
--text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;
11+
--green: #3fb950; --red: #f85149;
12+
}
13+
* { margin: 0; padding: 0; box-sizing: border-box; }
14+
body {
15+
background: var(--bg); color: var(--text);
16+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
17+
font-size: 14px;
18+
display: flex; justify-content: center; align-items: center;
19+
min-height: 100vh; padding: 24px;
20+
}
21+
.card {
22+
background: var(--surface); border: 1px solid var(--border);
23+
border-radius: 12px; padding: 32px; width: 100%; max-width: 480px;
24+
}
25+
h1 { font-size: 18px; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
26+
h1 .lock { font-size: 22px; }
27+
.subtitle { color: var(--muted); font-size: 12px; margin-bottom: 24px; }
28+
.form-group { margin-bottom: 16px; }
29+
.form-group label {
30+
display: block; color: var(--muted); font-size: 12px;
31+
margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px;
32+
}
33+
.form-group input, .form-group select {
34+
width: 100%; padding: 10px 12px; border-radius: 8px;
35+
border: 1px solid var(--border); background: var(--bg);
36+
color: var(--text); font-size: 14px; outline: none;
37+
}
38+
.form-group input:focus, .form-group select:focus {
39+
border-color: var(--accent);
40+
}
41+
.form-row { display: flex; gap: 12px; }
42+
.form-row .form-group { flex: 1; }
43+
.selectors { margin-top: 8px; padding: 12px; background: var(--bg); border-radius: 8px; border: 1px solid var(--border); }
44+
.selectors-title { font-size: 11px; color: var(--muted); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
45+
.selectors .form-group { margin-bottom: 8px; }
46+
.selectors .form-group:last-child { margin-bottom: 0; }
47+
.selectors input { font-size: 12px; padding: 6px 8px; font-family: monospace; }
48+
button[type="submit"] {
49+
width: 100%; padding: 12px; border-radius: 8px; border: none;
50+
background: var(--accent); color: var(--bg); font-size: 14px;
51+
font-weight: 600; cursor: pointer; margin-top: 8px;
52+
}
53+
button[type="submit"]:hover { opacity: 0.9; }
54+
button[type="submit"]:disabled { opacity: 0.5; cursor: not-allowed; }
55+
.success {
56+
text-align: center; padding: 48px 24px;
57+
}
58+
.success .check { font-size: 48px; margin-bottom: 16px; }
59+
.success h2 { color: var(--green); margin-bottom: 8px; }
60+
.success p { color: var(--muted); font-size: 13px; }
61+
.hidden { display: none; }
62+
.security-note {
63+
display: flex; align-items: flex-start; gap: 8px;
64+
background: rgba(63,185,80,0.08); border: 1px solid rgba(63,185,80,0.2);
65+
border-radius: 8px; padding: 10px 12px; margin-bottom: 20px;
66+
font-size: 12px; color: var(--green);
67+
}
68+
.security-note .icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
69+
</style>
70+
</head>
71+
<body>
72+
73+
<div class="card">
74+
<div id="form-view">
75+
<h1><span class="lock">&#128274;</span> Add Credential</h1>
76+
<p class="subtitle">Your password goes directly to the encrypted vault. The AI agent never sees it.</p>
77+
78+
<div class="security-note">
79+
<span class="icon">&#9989;</span>
80+
<span>This form submits directly to your local Vault. Credentials are encrypted with AES-256-GCM and never sent to any AI model.</span>
81+
</div>
82+
83+
<form id="add-form" onsubmit="return submitForm(event)">
84+
<div class="form-row">
85+
<div class="form-group">
86+
<label>Site ID</label>
87+
<input name="siteId" id="siteId" required placeholder="github">
88+
</div>
89+
<div class="form-group">
90+
<label>Type</label>
91+
<select name="serviceType" id="serviceType" onchange="toggleType(this.value)">
92+
<option value="web_login">Web Login</option>
93+
<option value="api_key">API Key</option>
94+
</select>
95+
</div>
96+
</div>
97+
98+
<div id="web-fields">
99+
<div class="form-group">
100+
<label>Email / Username</label>
101+
<input name="email" placeholder="user@example.com">
102+
</div>
103+
<div class="form-group">
104+
<label>Password</label>
105+
<input name="password" type="password" required>
106+
</div>
107+
<div class="form-group">
108+
<label>Login URL</label>
109+
<input name="loginUrl" placeholder="https://example.com/login">
110+
</div>
111+
<div class="selectors">
112+
<div class="selectors-title">CSS Selectors (auto-detected if left default)</div>
113+
<div class="form-group">
114+
<label>Email field</label>
115+
<input name="emailSel" value='input[type="email"]'>
116+
</div>
117+
<div class="form-group">
118+
<label>Password field</label>
119+
<input name="passwordSel" value='input[type="password"]'>
120+
</div>
121+
<div class="form-group">
122+
<label>Submit button</label>
123+
<input name="submitSel" value='button[type="submit"]'>
124+
</div>
125+
</div>
126+
</div>
127+
128+
<div id="api-fields" class="hidden">
129+
<div class="form-group">
130+
<label>API Key</label>
131+
<input name="apiKey" type="password">
132+
</div>
133+
<div class="form-row">
134+
<div class="form-group">
135+
<label>Header name</label>
136+
<input name="headerName" value="Authorization">
137+
</div>
138+
<div class="form-group">
139+
<label>Value prefix</label>
140+
<input name="headerPrefix" value="Bearer ">
141+
</div>
142+
</div>
143+
</div>
144+
145+
<button type="submit" id="submit-btn">Add to Vault</button>
146+
</form>
147+
</div>
148+
149+
<div id="success-view" class="success hidden">
150+
<div class="check">&#9989;</div>
151+
<h2>Credential Saved</h2>
152+
<p>Encrypted and stored locally. You can close this tab.<br>The AI agent will now be able to use <strong id="saved-site"></strong> without seeing your password.</p>
153+
</div>
154+
</div>
155+
156+
<script>
157+
const params = new URLSearchParams(location.search);
158+
const token = params.get('token') || '';
159+
const prefillSite = params.get('site') || '';
160+
const prefillType = params.get('type') || 'web_login';
161+
162+
if (prefillSite) document.getElementById('siteId').value = prefillSite;
163+
if (prefillType) {
164+
document.getElementById('serviceType').value = prefillType;
165+
toggleType(prefillType);
166+
}
167+
168+
function toggleType(type) {
169+
document.getElementById('web-fields').classList.toggle('hidden', type !== 'web_login');
170+
document.getElementById('api-fields').classList.toggle('hidden', type !== 'api_key');
171+
}
172+
173+
async function submitForm(e) {
174+
e.preventDefault();
175+
const btn = document.getElementById('submit-btn');
176+
btn.disabled = true;
177+
btn.textContent = 'Encrypting...';
178+
179+
const f = new FormData(e.target);
180+
const type = f.get('serviceType');
181+
const body = { siteId: f.get('siteId'), serviceType: type, token };
182+
183+
if (type === 'web_login') {
184+
Object.assign(body, {
185+
email: f.get('email'), password: f.get('password'),
186+
loginUrl: f.get('loginUrl'),
187+
selectors: { email: f.get('emailSel'), password: f.get('passwordSel'), submit: f.get('submitSel') }
188+
});
189+
} else {
190+
Object.assign(body, {
191+
apiKey: f.get('apiKey'), headerName: f.get('headerName'), headerPrefix: f.get('headerPrefix')
192+
});
193+
}
194+
195+
try {
196+
const res = await fetch('/api/credentials', {
197+
method: 'POST',
198+
headers: { 'Content-Type': 'application/json' },
199+
body: JSON.stringify(body)
200+
});
201+
202+
if (res.ok) {
203+
document.getElementById('saved-site').textContent = body.siteId;
204+
document.getElementById('form-view').classList.add('hidden');
205+
document.getElementById('success-view').classList.remove('hidden');
206+
} else {
207+
btn.disabled = false;
208+
btn.textContent = 'Add to Vault';
209+
alert('Error saving credential');
210+
}
211+
} catch (err) {
212+
btn.disabled = false;
213+
btn.textContent = 'Add to Vault';
214+
alert('Connection error: ' + err.message);
215+
}
216+
}
217+
</script>
218+
</body>
219+
</html>

0 commit comments

Comments
 (0)