Menu
Public repository / e19d3bd001a2452c1602304c070ad0d24c7d5860
AkurAI-PassVault / crates/passvault-mcp/src/server.rs
Latest change e19d3bd001a2452c1602304c070ad0d24c7d5860 - Add framework metadata to health endpoint by Ólafur Búi Ólafsson
use crate::tools::ToolRegistry;
use akurai_http::form::{field, parse_urlencoded};
use akurai_http::middleware_builtins::SecurityHeaders;
use akurai_http::{MiddlewareStack, Response, Server};
use akurai_storage::BTree;
use parking_lot::Mutex as PLMutex;
use passvault_core::crypto::Crypto;
use rand::Rng;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
fn hex32() -> String {
let mut rng = rand::thread_rng();
(0..32)
.map(|_| format!("{:02x}", rng.gen::<u8>()))
.collect()
}
fn parse_cookies(header: &str) -> HashMap<String, String> {
header
.split(';')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let key = parts.next()?.trim().to_string();
let val = parts.next().unwrap_or("").trim().to_string();
Some((key, val))
})
.collect()
}
const LANDING_HTML: &str = r##"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Encrypted secrets you control, with CLI and MCP access.">
<title>AkurAI-PassVault — secrets you control</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://akurai-framework.olibuijr.com/themes.css">
<script>
(function () {
try {
var m = document.cookie.match(/(?:^|; )akurai-theme=([^;]*)/);
var t = (m ? decodeURIComponent(m[1]) : null) || localStorage.getItem("akurai-theme");
if (!t) t = matchMedia("(prefers-color-scheme: light)").matches ? "akurai-light" : "akurai";
document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<script type="module" src="https://akurai-framework.olibuijr.com/theme.js"></script>
<style>
:root { --sans: Inter, ui-sans-serif, system-ui, sans-serif; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { min-width: 320px; margin: 0; background: var(--bg); color: var(--fg); font: 400 16px/1.6 var(--sans); -webkit-font-smoothing: antialiased; }
a { color: inherit; text-decoration: none; }
button { font: inherit; }
.shell { width: min(1120px, calc(100% - 40px)); margin: 0 auto; }
.nav { display: flex; align-items: center; justify-content: space-between; min-height: 76px; gap: 24px; }
.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--fg); font-size: 1.1rem; font-weight: 700; letter-spacing: -.04em; }
.mark { width: 30px; height: 30px; color: var(--accent-2); }
.mark svg { display: block; width: 100%; height: 100%; }
.brand-dim, .nav-link { color: var(--muted); font-weight: 500; }
.nav-links { display: flex; align-items: center; gap: 20px; }
.nav-link { font-size: .9rem; }
.nav-link:hover { color: var(--fg); }
.theme-control { position: relative; }
.theme-toggle, .theme-option { appearance: none; border: 1px solid var(--border); background: var(--panel); color: var(--fg); cursor: pointer; }
.theme-toggle { display: inline-flex; align-items: center; gap: 7px; min-height: 35px; padding: 0 10px; border-radius: 9px; font-size: .78rem; font-weight: 600; }
.theme-toggle:hover, .theme-option:hover { border-color: var(--accent); }
.theme-toggle::before { width: 9px; height: 9px; border: 2px solid var(--accent-2); border-radius: 50%; content: ""; }
.theme-menu { position: absolute; z-index: 2; top: calc(100% + 9px); right: 0; display: grid; grid-template-columns: repeat(2, minmax(120px, 1fr)); gap: 6px; width: 280px; padding: 8px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg-2); box-shadow: var(--shadow); }
.theme-menu[hidden] { display: none; }
.theme-option { min-height: 34px; border-radius: 7px; font-size: .76rem; text-align: left; }
.theme-option.is-active { border-color: var(--accent); color: var(--accent-2); }
.hero { position: relative; overflow: hidden; padding: 92px 0 108px; border-bottom: 1px solid var(--border); isolation: isolate; }
.hero::before { position: absolute; z-index: -1; top: -280px; left: 50%; width: 780px; height: 560px; transform: translateX(-50%); background: radial-gradient(ellipse, color-mix(in srgb, var(--accent) 34%, transparent), transparent 68%); content: ""; }
.eyebrow { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--border); border-radius: 999px; color: var(--muted); font-size: .77rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
.eyebrow::before { width: 7px; height: 7px; border-radius: 50%; background: var(--accent-2); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent-2) 16%, transparent); content: ""; }
h1 { max-width: 800px; margin: 24px 0 20px; font-size: clamp(2.7rem, 7vw, 5.6rem); line-height: .98; letter-spacing: -.075em; }
.grad { background: linear-gradient(100deg, var(--accent-2), var(--fg) 48%, var(--accent)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.lede { max-width: 650px; margin: 0; color: var(--muted); font-size: clamp(1.05rem, 2vw, 1.25rem); }
.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; }
.button { display: inline-flex; align-items: center; justify-content: center; min-height: 46px; padding: 0 19px; border: 1px solid var(--border); border-radius: 10px; font-weight: 600; transition: transform .2s, border-color .2s, background .2s; }
.button:hover { transform: translateY(-2px); border-color: var(--accent); }
.button-primary { border-color: var(--accent); background: var(--accent); color: var(--bg); box-shadow: 0 12px 32px color-mix(in srgb, var(--accent) 30%, transparent); }
.button-secondary { color: var(--muted); }
.security-note { display: flex; align-items: center; gap: 9px; margin-top: 27px; color: var(--muted); font-size: .87rem; }
.lock { color: var(--accent-2); }
.features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; padding: 76px 0; }
.feature { min-height: 255px; padding: 27px; border: 1px solid var(--border); border-radius: 16px; background: var(--panel); box-shadow: var(--shadow); }
.feature-icon { display: grid; width: 43px; height: 43px; place-items: center; border-radius: 12px; background: color-mix(in srgb, var(--accent) 18%, var(--panel)); color: var(--accent-2); font-size: 1.25rem; font-weight: 700; }
.feature h2 { margin: 25px 0 8px; font-size: 1.15rem; letter-spacing: -.035em; }
.feature p { margin: 0; color: var(--muted); font-size: .93rem; }
footer { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 26px 0 36px; border-top: 1px solid var(--border); color: var(--muted); font-size: .84rem; }
footer strong { color: var(--fg); }
@media (max-width: 720px) {
.shell { width: min(100% - 32px, 1120px); }
.nav { min-height: 68px; gap: 14px; }
.nav-links { gap: 12px; }
.nav-link { display: none; }
.hero { padding: 60px 0 76px; }
.features { grid-template-columns: 1fr; padding: 48px 0; }
.feature { min-height: auto; }
footer { align-items: flex-start; flex-direction: column; }
}
</style>
</head>
<body>
<header class="shell nav">
<a class="brand" href="/" aria-label="AkurAI PassVault home">
<span class="mark" aria-hidden="true"><svg viewBox="0 0 32 32" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.38 3.23c1.05-2.23 4.19-2.23 5.24 0l11.05 23.62A2.91 2.91 0 0 1 27.03 31h-3.42c-1.17 0-2.22-.7-2.67-1.78l-2.05-4.93h-5.78l-2.05 4.93A2.9 2.9 0 0 1 8.39 31H4.97a2.91 2.91 0 0 1-2.64-4.15L13.38 3.23ZM16 11.17l-5.13 13.1h10.26L16 11.17Z"/><path d="M10.38 20.54h11.24a2.23 2.23 0 0 1 0 4.46H10.38a2.23 2.23 0 0 1 0-4.46Z"/></svg></span>
<span>AkurAI<span class="brand-dim">/PassVault</span></span>
</a>
<nav class="nav-links" aria-label="Primary navigation">
<a class="nav-link" href="#features">Features</a>
<a class="nav-link" href="https://github.com/olibuijr/AkurAI-PassVault">Source</a>
<div class="theme-control">
<button class="theme-toggle" type="button" aria-expanded="false" aria-controls="theme-menu">Theme <span data-theme-label></span></button>
<div class="theme-menu" id="theme-menu" hidden>
<button class="theme-option" type="button" data-theme="akurai">AkurAI</button>
<button class="theme-option" type="button" data-theme="akurai-light">AkurAI Light</button>
<button class="theme-option" type="button" data-theme="claude-code">Claude Code</button>
<button class="theme-option" type="button" data-theme="claude-code-light">Claude Light</button>
<button class="theme-option" type="button" data-theme="nord">Nord</button>
<button class="theme-option" type="button" data-theme="nord-light">Nord Light</button>
<button class="theme-option" type="button" data-theme="catppuccin-mocha">Catppuccin Mocha</button>
<button class="theme-option" type="button" data-theme="catppuccin-latte">Catppuccin Latte</button>
<button class="theme-option" type="button" data-theme="solarized-dark">Solarized Dark</button>
<button class="theme-option" type="button" data-theme="solarized-light">Solarized Light</button>
<button class="theme-option" type="button" data-theme="gruvbox-dark">Gruvbox Dark</button>
<button class="theme-option" type="button" data-theme="gruvbox-light">Gruvbox Light</button>
<button class="theme-option" type="button" data-theme="tokyo-night">Tokyo Night</button>
<button class="theme-option" type="button" data-theme="tokyo-night-light">Tokyo Light</button>
<button class="theme-option" type="button" data-theme="rose-pine">Rosé Pine</button>
<button class="theme-option" type="button" data-theme="rose-pine-dawn">Rosé Pine Dawn</button>
<button class="theme-option" type="button" data-theme="dracula">Dracula</button>
</div>
</div>
</nav>
</header>
<main>
<section class="hero">
<div class="shell">
<span class="eyebrow">Private secret infrastructure</span>
<h1>AkurAI-PassVault — <span class="grad">secrets you control.</span></h1>
<p class="lede">Keep credentials encrypted in a vault you own. Retrieve only what a person, CLI, or MCP client needs—when it needs it.</p>
<div class="actions">
<a class="button button-primary" href="https://auth.olibuijr.com/login">Sign in with AkurAI <span aria-hidden="true"> →</span></a>
<a class="button button-secondary" href="https://github.com/olibuijr/AkurAI-PassVault">Get started</a>
</div>
<p class="security-note"><span class="lock" aria-hidden="true">⌁</span> Encrypted at rest. Your vault, your keys.</p>
</div>
</section>
<section class="shell features" id="features" aria-label="PassVault capabilities">
<article class="feature">
<div class="feature-icon" aria-hidden="true">⌁</div>
<h2>Encrypted storage</h2>
<p>Store sensitive values in a compact, encrypted vault designed to keep secret material under your control.</p>
</article>
<article class="feature">
<div class="feature-icon" aria-hidden="true">⌘</div>
<h2>CLI & MCP access</h2>
<p>Use the command line when you are working locally, or connect an MCP client over HTTPS when your tools need context.</p>
</article>
<article class="feature">
<div class="feature-icon" aria-hidden="true">↗</div>
<h2>Programmatic retrieval</h2>
<p>Give trusted automation the exact secret it needs without copying credentials into config files, logs, or chat.</p>
</article>
</section>
</main>
<footer class="shell">
<strong>AkurAI<span class="brand-dim">/PassVault</span></strong>
<span>Secrets you own. Access you control.</span>
<span>© 2026 AkurAI</span>
</footer>
<script>
(function () {
var toggle = document.querySelector(".theme-toggle");
var menu = document.getElementById("theme-menu");
var label = document.querySelector("[data-theme-label]");
var options = document.querySelectorAll(".theme-option");
function current() { return document.documentElement.getAttribute("data-theme") || "akurai"; }
function sync() {
var active = current();
options.forEach(function (option) {
var selected = option.getAttribute("data-theme") === active;
option.classList.toggle("is-active", selected);
option.setAttribute("aria-pressed", selected);
if (selected) label.textContent = option.textContent;
});
}
function close() { menu.hidden = true; toggle.setAttribute("aria-expanded", "false"); }
toggle.addEventListener("click", function () {
var open = menu.hidden;
menu.hidden = !open;
toggle.setAttribute("aria-expanded", open);
});
options.forEach(function (option) {
option.addEventListener("click", function () {
var theme = option.getAttribute("data-theme");
document.documentElement.setAttribute("data-theme", theme);
try {
localStorage.setItem("akurai-theme", theme);
document.cookie = "akurai-theme=" + encodeURIComponent(theme) + "; path=/; max-age=31536000; samesite=lax; domain=.olibuijr.com; secure";
} catch (e) {}
sync();
close();
});
});
document.addEventListener("click", function (event) {
if (!event.target.closest(".theme-control")) close();
});
document.addEventListener("keydown", function (event) { if (event.key === "Escape") close(); });
sync();
})();
</script>
</body>
</html>"##;
const LOGIN_HTML: &str = r##"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in — AkurAI-PassVault</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://akurai-framework.olibuijr.com/themes.css">
<script>
(function () {
try {
var m = document.cookie.match(/(?:^|; )akurai-theme=([^;]*)/);
var t = (m ? decodeURIComponent(m[1]) : null) || localStorage.getItem("akurai-theme");
if (!t) t = matchMedia("(prefers-color-scheme: light)").matches ? "akurai-light" : "akurai";
document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<script type="module" src="https://akurai-framework.olibuijr.com/theme.js"></script>
<style>
:root { --sans: Inter, ui-sans-serif, system-ui, sans-serif; }
* { box-sizing: border-box; }
body { min-width: 320px; margin: 0; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: var(--bg); color: var(--fg); font: 400 16px/1.6 var(--sans); -webkit-font-smoothing: antialiased; }
.card { width: min(400px, calc(100% - 32px)); padding: 40px 32px; border: 1px solid var(--border); border-radius: 16px; background: var(--panel); box-shadow: var(--shadow); }
.mark { width: 36px; height: 36px; color: var(--accent-2); display: block; margin-bottom: 18px; }
h1 { margin: 0 0 6px; font-size: 1.5rem; letter-spacing: -.03em; }
p { margin: 0 0 24px; color: var(--muted); font-size: .9rem; }
label { display: block; margin-bottom: 18px; font-size: .85rem; font-weight: 500; color: var(--muted); }
input { display: block; width: 100%; margin-top: 6px; padding: 11px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg); color: var(--fg); font: inherit; font-size: .95rem; }
input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
.btn { display: inline-flex; align-items: center; justify-content: center; width: 100%; min-height: 44px; padding: 0 18px; border: 1px solid var(--accent); border-radius: 10px; background: var(--accent); color: var(--bg); font-weight: 600; cursor: pointer; font: inherit; font-size: .95rem; }
.btn:hover { filter: brightness(1.08); }
.error { display: none; margin-bottom: 18px; padding: 11px 14px; border: 1px solid var(--danger); border-radius: 10px; background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); font-size: .85rem; }
.error.visible { display: block; }
</style>
</head>
<body>
<div class="card">
<svg class="mark" viewBox="0 0 32 32" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.38 3.23c1.05-2.23 4.19-2.23 5.24 0l11.05 23.62A2.91 2.91 0 0 1 27.03 31h-3.42c-1.17 0-2.22-.7-2.67-1.78l-2.05-4.93h-5.78l-2.05 4.93A2.9 2.9 0 0 1 8.39 31H4.97a2.91 2.91 0 0 1-2.64-4.15L13.38 3.23ZM16 11.17l-5.13 13.1h10.26L16 11.17Z"/><path d="M10.38 20.54h11.24a2.23 2.23 0 0 1 0 4.46H10.38a2.23 2.23 0 0 1 0-4.46Z"/></svg>
<h1>Sign in</h1>
<p>Enter your vault token to access the dashboard.</p>
<div class="error" id="error-msg"></div>
<form method="post" action="/login">
<label>Vault token<input type="password" name="token" placeholder="Bearer token" autocomplete="off" required /></label>
<button class="btn" type="submit">Sign in</button>
</form>
<p style="margin-top:18px;text-align:center;font-size:.8rem"><a href="/" style="color:var(--accent)">← Back to landing</a></p>
</div>
</body>
</html>"##;
const DASHBOARD_HTML_PREFIX: &str = r##"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AkurAI-PassVault</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://akurai-framework.olibuijr.com/themes.css">
<script>
(function () {
try {
var m = document.cookie.match(/(?:^|; )akurai-theme=([^;]*)/);
var t = (m ? decodeURIComponent(m[1]) : null) || localStorage.getItem("akurai-theme");
if (!t) t = matchMedia("(prefers-color-scheme: light)").matches ? "akurai-light" : "akurai";
document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<script type="module" src="https://akurai-framework.olibuijr.com/theme.js"></script>
<style>
:root { --sans: Inter, ui-sans-serif, system-ui, sans-serif; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { min-width: 320px; margin: 0; background: var(--bg); color: var(--fg); font: 400 15px/1.5 var(--sans); -webkit-font-smoothing: antialiased; overflow: hidden; height: 100vh; display: flex; flex-direction: column; }
a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; }
.topbar { display: flex; align-items: center; gap: 16px; height: 56px; padding: 0 20px; border-bottom: 1px solid var(--border); flex: none; }
.brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; font-size: 1rem; }
.brand .mark { width: 24px; height: 24px; color: var(--accent-2); flex: none; }
.brand-dim { color: var(--muted); font-weight: 500; }
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.search-box { position: relative; width: 240px; }
.search-box input { width: 100%; padding: 7px 12px 7px 32px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-2); color: var(--fg); font-size: .85rem; }
.search-box input:focus { outline: none; border-color: var(--accent); }
.search-box::before { content: ""; position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; border: 2px solid var(--muted); border-radius: 50%; }
.search-box::after { content: ""; position: absolute; left: 21px; top: calc(50% + 6px); width: 2px; height: 7px; background: var(--muted); transform: rotate(-45deg); }
.btn { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); color: var(--fg); font-size: .85rem; font-weight: 500; cursor: pointer; white-space: nowrap; }
.btn:hover { border-color: var(--accent); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--bg); font-weight: 600; }
.btn-danger { border-color: var(--danger); color: var(--danger); }
.btn-sm { min-height: 28px; padding: 0 10px; font-size: .8rem; }
.shell { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 220px; flex: none; border-right: 1px solid var(--border); padding: 12px; overflow-y: auto; }
.sidebar-heading { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; font-size: .88rem; cursor: pointer; color: var(--muted); }
.sidebar-item:hover { background: var(--panel); color: var(--fg); }
.sidebar-item.active { background: var(--panel); color: var(--fg); font-weight: 500; }
.sidebar-item .count { margin-left: auto; background: var(--border); border-radius: 999px; padding: 1px 7px; font-size: .75rem; color: var(--muted); }
.content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.entry-list { flex: 1; overflow-y: auto; padding: 12px 16px; }
.entry-row { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 12px; align-items: center; padding: 10px 14px; border: 1px solid transparent; border-radius: 10px; cursor: pointer; }
.entry-row:hover { background: var(--panel); border-color: var(--border); }
.entry-row.selected { background: var(--panel); border-color: var(--accent); }
.entry-name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.entry-meta { color: var(--muted); font-size: .83rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.folder-badge { display: inline-flex; padding: 1px 7px; border-radius: 999px; background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent-2); font-size: .75rem; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; color: var(--muted); text-align: center; padding: 40px; }
.empty-state h2 { margin: 12px 0 6px; font-size: 1.3rem; color: var(--fg); }
.empty-state p { max-width: 360px; font-size: .9rem; }
.detail-panel { width: 0; overflow: hidden; border-left: 1px solid var(--border); transition: width .2s; }
.detail-panel.open { width: 380px; overflow-y: auto; }
.detail-inner { width: 380px; padding: 20px; }
.detail-inner h2 { margin: 0 0 18px; font-size: 1.1rem; }
.detail-inner label { display: block; margin-bottom: 14px; font-size: .8rem; font-weight: 500; color: var(--muted); }
.detail-inner input, .detail-inner textarea, .detail-inner select { display: block; width: 100%; margin-top: 5px; padding: 8px 11px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--fg); font-size: .88rem; }
.detail-inner input:focus, .detail-inner textarea:focus { outline: none; border-color: var(--accent); }
.detail-inner textarea { resize: vertical; min-height: 60px; }
.detail-inner select { cursor: pointer; }
.detail-actions { display: flex; gap: 8px; margin-top: 18px; flex-wrap: wrap; }
.toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 18px; border-radius: 10px; background: var(--panel); border: 1px solid var(--border); box-shadow: var(--shadow); font-size: .85rem; z-index: 100; opacity: 0; transform: translateY(10px); transition: opacity .25s, transform .25s; pointer-events: none; }
.toast.visible { opacity: 1; transform: translateY(0); }
.toast.ok { border-color: var(--ok); }
.toast.err { border-color: var(--danger); }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 200; }
.modal-overlay[hidden] { display: none; }
.modal { width: min(420px, calc(100% - 32px)); padding: 28px; border: 1px solid var(--border); border-radius: 16px; background: var(--panel); box-shadow: var(--shadow); }
.modal h2 { margin: 0 0 8px; font-size: 1.15rem; }
.modal p { color: var(--muted); font-size: .9rem; margin: 0 0 20px; }
.pw-gen { display: flex; gap: 8px; align-items: center; margin-top: 5px; }
.pw-gen input { flex: 1; }
.pw-gen .btn { flex: none; }
.health-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--ok); }
</style>
</head>
<body>
<div class="topbar">
<a class="brand" href="/app" aria-label="AkurAI PassVault home">
<svg class="mark" viewBox="0 0 32 32" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.38 3.23c1.05-2.23 4.19-2.23 5.24 0l11.05 23.62A2.91 2.91 0 0 1 27.03 31h-3.42c-1.17 0-2.22-.7-2.67-1.78l-2.05-4.93h-5.78l-2.05 4.93A2.9 2.9 0 0 1 8.39 31H4.97a2.91 2.91 0 0 1-2.64-4.15L13.38 3.23ZM16 11.17l-5.13 13.1h10.26L16 11.17Z"/><path d="M10.38 20.54h11.24a2.23 2.23 0 0 1 0 4.46H10.38a2.23 2.23 0 0 1 0-4.46Z"/></svg>
AkurAI<span class="brand-dim">/PassVault</span>
</a>
<div class="search-box"><input type="text" id="search-input" placeholder="Search entries…" aria-label="Search vault entries"></div>
<div class="topbar-actions">
<span class="health-dot" id="health-dot" title="Connected"></span>
<button class="btn btn-primary btn-sm" id="new-entry-btn" type="button">+ New entry</button>
<button class="btn btn-sm" id="generate-pw-btn" type="button">Generate</button>
<span data-theme-picker></span>
<a class="btn btn-sm" href="/logout">Sign out</a>
</div>
</div>
<div class="shell">
<aside class="sidebar" id="sidebar">
<div class="sidebar-heading"><span>Folders</span><button class="btn btn-sm" id="new-folder-btn" type="button" style="border:0;padding:2px 6px;font-size:.8rem">+ Add</button></div>
<div class="sidebar-item active" data-folder-id="__all__"><span>📁</span> All entries<span class="count" id="all-count">0</span></div>
<div id="folder-list"></div>
</aside>
<main class="content" id="main-content">
<div class="entry-list" id="entry-list"></div>
</main>
<aside class="detail-panel" id="detail-panel">
<div class="detail-inner" id="detail-inner"></div>
</aside>
</div>
<div class="toast" id="toast"></div>
<div class="modal-overlay" id="modal-overlay" hidden>
<div class="modal" id="modal-content"></div>
</div>
<script>
const TOKEN = '@@TOKEN@@';
const API = '/mcp';
async function mcp(method, args = {}) {
const res = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: method, arguments: args } })
});
const data = await res.json();
if (data.error) throw new Error(data.error.message);
return JSON.parse(data.result.content[0].text);
}
const state = { entries: [], folders: [], folderId: null, selectedId: null, renderedEntries: [], searchTimer: null };
const entryList = document.getElementById('entry-list');
const folderList = document.getElementById('folder-list');
const detailPanel = document.getElementById('detail-panel');
const detailInner = document.getElementById('detail-inner');
const healthDot = document.getElementById('health-dot');
const modalOverlay = document.getElementById('modal-overlay');
const modalContent = document.getElementById('modal-content');
const toast = document.getElementById('toast');
let toastTimer;
function escapeHtml(value = '') {
return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]);
}
function folderOf(entry) {
return entry.folderId ?? entry.folder_id ?? null;
}
function setHealth(connected) {
healthDot.style.background = connected ? 'var(--ok)' : 'var(--danger)';
healthDot.title = connected ? 'Connected' : 'Connection error';
}
function errorMessage(error) {
return error instanceof Error ? error.message : 'Something went wrong';
}
function showToast(msg, type = 'ok') {
clearTimeout(toastTimer);
toast.textContent = msg;
toast.className = `toast ${type === 'err' ? 'err' : 'ok'}`;
requestAnimationFrame(() => toast.classList.add('visible'));
toastTimer = setTimeout(() => toast.classList.remove('visible'), 2000);
}
function reportError(error) {
setHealth(false);
showToast(errorMessage(error), 'err');
}
function hideModal() {
modalOverlay.hidden = true;
modalContent.innerHTML = '';
}
function showModal(html, confirmCb, confirmLabel = 'Confirm') {
modalContent.innerHTML = `${html}<div class="detail-actions"><button class="btn" id="modal-cancel" type="button">Cancel</button><button class="btn btn-primary" id="modal-confirm" type="button">${escapeHtml(confirmLabel)}</button></div>`;
modalOverlay.hidden = false;
document.getElementById('modal-cancel').onclick = hideModal;
document.getElementById('modal-confirm').onclick = async event => {
const button = event.currentTarget;
button.disabled = true;
try {
await confirmCb();
hideModal();
} catch (error) {
reportError(error);
button.disabled = false;
}
};
}
function closeDetail() {
state.selectedId = null;
detailPanel.classList.remove('open');
detailInner.innerHTML = '';
renderEntries(state.renderedEntries);
}
function showEmptyState() {
state.renderedEntries = [];
entryList.innerHTML = '<div class="empty-state"><div aria-hidden="true">🔐</div><h2>No entries yet</h2><p>Add a vault entry to keep credentials securely organized.</p><button class="btn btn-primary" id="empty-new-entry" type="button">+ New entry</button></div>';
document.getElementById('empty-new-entry').onclick = showNewEntryForm;
}
function renderEntries(entries) {
state.renderedEntries = entries;
if (!entries.length) {
showEmptyState();
return;
}
entryList.innerHTML = entries.map(entry => {
const folderId = folderOf(entry);
const folder = state.folders.find(item => item.id === folderId);
const badge = folderId == null ? '' : `<span class="folder-badge">${escapeHtml(folder ? folder.name : `Folder ${folderId}`)}</span>`;
return `<div class="entry-row${state.selectedId === entry.id ? ' selected' : ''}" data-entry-id="${entry.id}" role="button" tabindex="0"><span class="entry-name">${escapeHtml(entry.name)}</span><span class="entry-meta">${escapeHtml(entry.username)}</span><span class="entry-meta">${escapeHtml(entry.uri)}</span>${badge}</div>`;
}).join('');
entryList.querySelectorAll('.entry-row').forEach(row => {
const open = () => showDetail(Number(row.dataset.entryId));
row.onclick = open;
row.onkeydown = event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
open();
}
};
});
}
function renderFolders() {
document.getElementById('all-count').textContent = String(state.entries.length);
document.querySelector('[data-folder-id="__all__"]').classList.toggle('active', state.folderId === null);
folderList.innerHTML = state.folders.map(folder => {
const count = state.entries.filter(entry => folderOf(entry) === folder.id).length;
return `<div class="sidebar-item${state.folderId === folder.id ? ' active' : ''}" data-folder-id="${folder.id}" role="button" tabindex="0"><span>📁</span>${escapeHtml(folder.name)}<span class="count">${count}</span></div>`;
}).join('');
folderList.querySelectorAll('.sidebar-item').forEach(item => {
const select = () => {
state.selectedId = null;
closeDetail();
loadEntries(Number(item.dataset.folderId)).catch(reportError);
};
item.onclick = select;
item.onkeydown = event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
select();
}
};
});
}
async function loadEntries(folderId) {
if (folderId !== undefined) state.folderId = folderId;
state.entries = await mcp('list_entries');
const entries = state.folderId === null
? state.entries
: state.entries.filter(entry => folderOf(entry) === state.folderId);
renderEntries(entries);
renderFolders();
setHealth(true);
}
async function loadFolders() {
state.folders = await mcp('list_folders');
renderFolders();
renderEntries(state.renderedEntries);
setHealth(true);
}
function folderOptions(selectedId) {
return `<option value="">No folder</option>${state.folders.map(folder => `<option value="${folder.id}"${folder.id === selectedId ? ' selected' : ''}>${escapeHtml(folder.name)}</option>`).join('')}`;
}
function entryValues(form) {
const data = new FormData(form);
const folderId = data.get('folderId');
return {
name: String(data.get('name') || '').trim(),
username: String(data.get('username') || ''),
password: String(data.get('password') || ''),
uri: String(data.get('uri') || ''),
notes: String(data.get('notes') || ''),
...(folderId ? { folderId: Number(folderId) } : {})
};
}
function renderEntryForm(entry) {
const isNew = !entry;
const value = field => escapeHtml(entry ? entry[field] : '');
detailInner.innerHTML = `<h2>${isNew ? 'New entry' : 'Entry details'}</h2><form id="entry-form" novalidate><label>Name<input name="name" value="${value('name')}" autocomplete="off"></label><label>Username<input name="username" value="${value('username')}" autocomplete="username"></label><label>Password<div class="pw-gen"><input id="entry-password" name="password" type="password" value="${value('password')}" autocomplete="new-password"><button class="btn btn-sm" id="toggle-password" type="button">Show</button></div></label><label>URI<input name="uri" type="url" value="${value('uri')}" autocomplete="url"></label><label>Notes<textarea name="notes">${value('notes')}</textarea></label><label>Folder<select name="folderId">${folderOptions(entry ? folderOf(entry) : null)}</select></label><div class="detail-actions"><button class="btn btn-primary" type="submit">Save</button>${isNew ? '' : '<button class="btn btn-danger" id="delete-entry" type="button">Delete</button>'}</div></form>`;
detailPanel.classList.add('open');
const form = document.getElementById('entry-form');
const password = document.getElementById('entry-password');
document.getElementById('toggle-password').onclick = event => {
const showing = password.type === 'text';
password.type = showing ? 'password' : 'text';
event.currentTarget.textContent = showing ? 'Show' : 'Hide';
};
form.onsubmit = async event => {
event.preventDefault();
const values = entryValues(form);
if (!values.name) {
showToast('A name is required', 'err');
return;
}
try {
await mcp(isNew ? 'create_entry' : 'update_entry', isNew ? values : { id: entry.id, ...values });
showToast(isNew ? 'Entry created' : 'Entry saved');
closeDetail();
await refresh();
setHealth(true);
} catch (error) {
reportError(error);
}
};
if (!isNew) {
document.getElementById('delete-entry').onclick = () => showModal('<h2>Delete entry?</h2><p>This permanently removes the entry from your vault.</p>', async () => {
await mcp('delete_entry', { id: entry.id });
showToast('Entry deleted');
closeDetail();
await refresh();
setHealth(true);
}, 'Delete');
}
}
async function showDetail(entryId) {
try {
const entry = await mcp('get_entry', { id: entryId });
state.selectedId = entryId;
renderEntries(state.renderedEntries);
renderEntryForm(entry);
setHealth(true);
} catch (error) {
reportError(error);
}
}
function showNewEntryForm() {
state.selectedId = null;
renderEntries(state.renderedEntries);
renderEntryForm(null);
}
async function refresh() {
await Promise.all([loadEntries(), loadFolders()]);
}
document.querySelector('[data-folder-id="__all__"]').onclick = () => {
state.selectedId = null;
closeDetail();
loadEntries(null).catch(reportError);
};
document.getElementById('new-entry-btn').onclick = showNewEntryForm;
document.getElementById('generate-pw-btn').onclick = async () => {
try {
const { password } = await mcp('generate_password', {});
showModal(`<h2>Generated password</h2><p>Copy it now, then save it in a vault entry.</p><div class="pw-gen"><input id="generated-password" type="text" value="${escapeHtml(password)}" readonly></div>`, async () => {
await navigator.clipboard.writeText(password);
showToast('Password copied');
}, 'Copy');
setHealth(true);
} catch (error) {
reportError(error);
}
};
document.getElementById('new-folder-btn').onclick = () => showModal('<h2>New folder</h2><p>Choose a name to organize your entries.</p><input id="new-folder-name" aria-label="Folder name" style="width:100%;padding:8px 11px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--fg)">', async () => {
const name = document.getElementById('new-folder-name').value.trim();
if (!name) throw new Error('A folder name is required');
await mcp('create_folder', { name });
await refresh();
showToast('Folder created');
setHealth(true);
}, 'Create');
document.getElementById('search-input').oninput = event => {
clearTimeout(state.searchTimer);
const query = event.currentTarget.value.trim();
state.searchTimer = setTimeout(async () => {
try {
if (!query) {
await loadEntries();
return;
}
const entries = await mcp('search_entries', { query });
if (document.getElementById('search-input').value.trim() === query) {
state.selectedId = null;
closeDetail();
renderEntries(entries);
setHealth(true);
}
} catch (error) {
reportError(error);
}
}, 300);
};
modalOverlay.onclick = event => {
if (event.target === modalOverlay) hideModal();
};
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && !modalOverlay.hidden) hideModal();
});
(async () => {
try {
await refresh();
} catch (error) {
reportError(error);
}
})();
</script>
</body>
</html>"##;
pub struct McpConfig {
pub host: String,
pub port: u16,
pub db_path: String,
pub token: String,
pub encryption_key: String,
}
pub struct McpServer {
config: McpConfig,
}
impl McpServer {
pub fn new(config: McpConfig) -> Self {
McpServer { config }
}
pub fn serve(&self) -> Result<(), String> {
let db =
BTree::open(&self.config.db_path).map_err(|e| format!("failed to open db: {e}"))?;
let db = Arc::new(Mutex::new(db));
let crypto = Crypto::from_hex_key(&self.config.encryption_key)
.map_err(|e| format!("crypto init: {e}"))?;
let crypto = Arc::new(crypto);
let token = self.config.token.clone();
let registry = Arc::new(ToolRegistry::new(db, crypto));
let sessions: Arc<PLMutex<HashMap<String, String>>> =
Arc::new(PLMutex::new(HashMap::new()));
let handler = move |req: &akurai_http::Request| -> Response {
if req.path == "/" && req.method == akurai_http::Method::Get {
return Response::new(200).with_html(LANDING_HTML);
}
if req.path == "/login" && req.method == akurai_http::Method::Get {
return Response::new(200).with_html(LOGIN_HTML);
}
if req.path == "/login" && req.method == akurai_http::Method::Post {
let body = req.body_str();
let pairs = parse_urlencoded(&body);
let submitted = field(&pairs, "token").unwrap_or("");
return if submitted == token {
let sid = hex32();
sessions.lock().insert(sid.clone(), token.clone());
Response::new(303)
.with_header("Location", "/app")
.with_header(
"Set-Cookie",
&format!(
"passvault_session={sid}; path=/; HttpOnly; SameSite=Lax; Secure"
),
)
.with_text("ok")
} else {
Response::new(200).with_html(&LOGIN_HTML.replace(
"id=\"error-msg\"></div>",
"id=\"error-msg\" class=\"visible\">Invalid token</div>",
))
};
}
if req.path == "/logout" && req.method == akurai_http::Method::Get {
let cookie = req.header("cookie").unwrap_or("");
let cookies = parse_cookies(cookie);
if let Some(sid) = cookies.get("passvault_session") {
sessions.lock().remove(sid);
}
return Response::new(303)
.with_header("Location", "/login")
.with_header(
"Set-Cookie",
"passvault_session=; path=/; HttpOnly; Max-Age=0",
)
.with_text("ok");
}
if req.path == "/app" && req.method == akurai_http::Method::Get {
let cookie = req.header("cookie").unwrap_or("");
let cookies = parse_cookies(cookie);
let sid = match cookies.get("passvault_session") {
Some(s) => s.clone(),
None => {
return Response::new(303)
.with_header("Location", "/login")
.with_text("redirect")
}
};
let sessions_guard = sessions.lock();
let sess_token = match sessions_guard.get(&sid) {
Some(t) => t.clone(),
None => {
return Response::new(303)
.with_header("Location", "/login")
.with_text("redirect")
}
};
drop(sessions_guard);
let html = DASHBOARD_HTML_PREFIX.replace("@@TOKEN@@", &sess_token);
return Response::new(200).with_html(&html);
}
if req.path == "/api/health" && req.method == akurai_http::Method::Get {
let body = format!(
r#"{{"app":"akurai-passvault","version":"{}","status":"ok","framework":"AkurAI-Framework","framework_version":"0.8.2"}}"#,
env!("CARGO_PKG_VERSION")
);
return Response::new(200)
.with_header("Content-Type", "application/json")
.with_body("application/json", body.into_bytes());
}
if req.path != "/mcp" || req.method != akurai_http::Method::Post {
return Response::new(404).with_text("not found");
}
// MCP auth: Bearer token or session cookie
let expected = format!("Bearer {token}");
let auth_hdr = req.header("authorization").unwrap_or("");
let auth_ok = auth_hdr == expected;
let session_ok = if !auth_ok {
let cookie = req.header("cookie").unwrap_or("");
let cookies = parse_cookies(cookie);
cookies
.get("passvault_session")
.and_then(|sid| sessions.lock().get(sid).cloned())
.map(|st| st == token)
.unwrap_or(false)
} else {
false
};
if !auth_ok && !session_ok {
return Response::new(401).with_text("unauthorized");
}
let body = req.body_str();
let result = registry.handle_request(&body);
// Empty result = JSON-RPC notification: acknowledge with 202 + empty body
// (MCP Streamable-HTTP spec). Strict clients (codex/rmcp) break if a
// notification receives a JSON-RPC response body.
if result.is_empty() {
return Response::new(202);
}
Response::new(200)
.with_header("Content-Type", "application/json")
.with_body("application/json", result.into_bytes())
};
let addr = format!("{}:{}", self.config.host, self.config.port);
eprintln!("AkurAI-PassVault listening on {addr}");
Server::bind(&addr)
.map_err(|e| format!("bind: {e}"))?
.run_with(MiddlewareStack::new().push(SecurityHeaders), handler)
.map_err(|e| format!("server: {e}"))
}
}