Install pages: OS-detect hasher download + remote MCP URL

Wire mcp.georgelambert.org / mcp.veraetime.net install UX to publish
hasher binaries, WASM fallback, and public /mcp endpoint without cloning.
This commit is contained in:
George Lambert 2026-09-25 13:30:30 -04:00
commit fc3d0866d8
3 changed files with 386 additions and 52 deletions

58
site/hasher.html Normal file
View file

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Verae WASM hasher</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.45; }
pre { background: #f4f4f4; padding: 0.8rem; white-space: pre-wrap; word-break: break-all; }
input[type=file] { display: block; margin: 1rem 0; }
</style>
</head>
<body>
<h1>Local WASM hasher</h1>
<p>Hash a file in this browser. Bytes never leave the device. Paste the digest into <a href="sample.html">sample.html</a> or your MCP client.</p>
<input type="file" id="f">
<pre id="out">Waiting for a file…</pre>
<script type="module">
const out = document.getElementById("out");
const bases = [
(location.origin || "") + "/hasher",
"https://mcp.georgelambert.org/hasher"
];
async function loadLatest() {
for (const b of bases) {
try {
const latest = await (await fetch(b + "/latest.json", {cache:"no-cache"})).json();
const manUrl = latest.manifest || (b + "/" + latest.version + "/manifest.json");
const man = await (await fetch(manUrl, {cache:"no-cache"})).json();
return man;
} catch (e) {}
}
throw new Error("cannot load hasher manifest");
}
const man = await loadLatest();
const base = man.base_url.replace(/\/$/, "");
const jsUrl = base + "/" + man.wasm.js;
out.textContent = "Loading " + jsUrl + " …";
const mod = await import(jsUrl);
await mod.default();
out.textContent = "Ready. Pick a file.";
document.getElementById("f").addEventListener("change", async (ev) => {
const file = ev.target.files && ev.target.files[0];
if (!file) return;
out.textContent = "Hashing " + file.name + " (" + file.size + " bytes)…";
const stream = new mod.Sha256Stream();
const reader = file.stream().getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
stream.update(value);
}
const digest = stream.finish();
out.textContent = digest + " " + file.name + "\n\nPaste into sample.html or stamp via MCP.";
});
</script>
</body>
</html>

View file

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Install Verae timestamping</title>
<meta name="description" content="Register a VeraeTime account, store the token on your machine, and connect Claude, Cursor, or another MCP client.">
<meta name="description" content="Download a local hasher, connect to the Verae MCP timestamping server, and stamp digests without uploading files.">
<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=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@ -23,6 +23,7 @@
--gold: oklch(0.80 0.090 85);
--dark: oklch(0.19 0.022 312);
--ease: cubic-bezier(0.16, 1, 0.3, 1);
--ok: oklch(0.55 0.12 145);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
@ -43,7 +44,7 @@
padding: 28px 28px 0;
}
.brand img { height: 46px; width: auto; display: block; }
nav { display: flex; gap: 22px; font-size: 14px; font-weight: 600; }
nav { display: flex; gap: 22px; font-size: 14px; font-weight: 600; flex-wrap: wrap; }
nav a { color: var(--ink-soft); text-decoration: none; }
nav a:hover { color: var(--purple); }
.hero {
@ -65,9 +66,9 @@
line-height: 1.05;
letter-spacing: -0.03em;
margin: 12px 0 18px;
max-width: 16ch;
max-width: 18ch;
}
.lede { max-width: 40rem; font-size: 18px; color: var(--ink-soft); margin: 0; }
.lede { max-width: 42rem; font-size: 18px; color: var(--ink-soft); margin: 0; }
.wrap { max-width: 1100px; margin: 0 auto; padding: 12px 28px 80px; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 36px; }
.card {
@ -85,7 +86,7 @@
font-weight: 560;
margin: 56px 0 8px;
}
.section-note { color: var(--ink-soft); max-width: 42rem; margin-top: 0; }
.section-note { color: var(--ink-soft); max-width: 44rem; margin-top: 0; }
pre {
background: var(--dark);
color: oklch(0.95 0.01 90);
@ -101,6 +102,51 @@
ol.steps { padding-left: 1.2rem; max-width: 44rem; }
ol.steps li { margin: 0.55rem 0; }
.gold { height: 3px; width: 72px; background: var(--gold); border: 0; margin: 28px 0; }
.detect {
background: white;
border: 1px solid var(--hair);
border-radius: 18px;
padding: 22px;
margin-top: 22px;
}
.detect .status { font-size: 15px; color: var(--ink-soft); margin: 0 0 14px; }
.detect .status strong { color: var(--ink); }
.btn-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--purple);
color: white;
text-decoration: none;
border: 0;
border-radius: 999px;
padding: 12px 18px;
font: inherit;
font-weight: 600;
font-size: 14px;
cursor: pointer;
}
.btn.secondary {
background: white;
color: var(--purple);
border: 1px solid var(--hair);
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.meta { font-size: 13px; color: var(--ink-mute); margin-top: 12px; }
.ok { color: var(--ok); font-weight: 600; }
table.artifacts {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
font-size: 14px;
}
table.artifacts th, table.artifacts td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid var(--hair);
}
table.artifacts th { color: var(--ink-mute); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
footer {
border-top: 1px solid var(--hair);
padding: 28px;
@ -119,91 +165,105 @@
<header>
<a class="brand" href="https://www.verae.com/"><img src="assets/VeraeFullLogo.png" alt="Verae"></a>
<nav>
<a href="#hasher">Hasher</a>
<a href="#connect">Connect</a>
<a href="#register">Register</a>
<a href="#clients">Clients</a>
<a href="#token">Token</a>
<a href="sample.html">Try a stamp</a>
</nav>
</header>
<main>
<section class="hero">
<p class="eyebrow">Model Context Protocol</p>
<h1>Install Verae timestamping.</h1>
<p class="lede">Register once. The token stays in a private file on your machine, or in your assistant’s own login store. It does not go in the chat, and it does not go in the client config.</p>
<h1>Hash locally. Stamp remotely.</h1>
<p class="lede">Download a small <code>verae-sha256</code> binary for your OS (or use WASM on mobile/browser). Digests go to the MCP server. File bytes stay on your machine.</p>
<hr class="gold">
</section>
<div class="wrap">
<section id="hasher">
<h2 class="section">Get the local hasher</h2>
<p class="section-note">No repo clone. This page detects your platform, fetches the matching published binary from <code>/hasher/</code>, and falls back to WASM/JS when a native build is not available.</p>
<div class="detect" id="hasher-panel">
<p class="status" id="hasher-status">Detecting platform…</p>
<div class="btn-row" id="hasher-actions"></div>
<p class="meta" id="hasher-meta"></p>
<pre id="hasher-snippet" hidden></pre>
</div>
<h3 style="margin-top:36px;font-size:18px;">All published binaries</h3>
<table class="artifacts" id="artifact-table">
<thead><tr><th>Target</th><th>File</th><th>Size</th><th>SHA-256</th></tr></thead>
<tbody></tbody>
</table>
<p class="meta">Manifest: <a id="manifest-link" href="/hasher/latest.json">/hasher/latest.json</a> · Source rebuild: <code>./scripts/publish-hasher.sh</code> in the client repo.</p>
</section>
<section id="connect">
<h2 class="section">Connect an MCP client</h2>
<p class="section-note">Point Claude, Cursor, or another MCP client at this host. You do not need to run a local server.</p>
<pre id="mcp-config">{
"mcpServers": {
"verae-timestamping": {
"url": "https://HOST/mcp"
}
}
}</pre>
<p class="section-note">Public endpoints: <code id="mcp-url-primary">https://HOST/mcp</code>
· also <a href="https://mcp.veraetime.net/mcp">mcp.veraetime.net/mcp</a>
· <a href="https://mcp.georgelambert.org/mcp">mcp.georgelambert.org/mcp</a></p>
<h2 class="section">Hash a file, then stamp</h2>
<pre id="hash-flow"># after installing the binary as verae-sha256 on your PATH:
DIGEST=$(verae-sha256 ./document.pdf)
echo "$DIGEST"
# then ask your MCP client to call tool "stamp" with { "sha256": "$DIGEST", "scope": "global" }
# or try the browser sample:
# open sample.html and paste the digest</pre>
</section>
<section id="register">
<h2 class="section">Register on your machine</h2>
<p class="section-note">This is the install for a person or a server you control. <code>uv</code> runs the command. You do not clone this repository.</p>
<h2 class="section">Optional: register on your machine</h2>
<p class="section-note">For a local <code>uvx</code> server (or global stamps that need a VeraeTime account), register once. You still do not clone a repository.</p>
<pre>uvx verae-mcp-timestamping-server setup</pre>
<ol class="steps">
<li>Choose <strong>new</strong> to create a VeraeTime account, or <strong>existing</strong> to log in.</li>
<li>Enter a username and password. The command calls <code>https://api.veraetime.net</code>.</li>
<li>It writes <span class="path">~/.verae/auth.properties</span> with mode <code>600</code> and prints your username, role, and expiry. It does not print the token.</li>
<li>Check it later with <code>uvx verae-mcp-timestamping-server session</code>.</li>
<li>It writes <span class="path">~/.verae/auth.properties</span> with mode <code>600</code>. It does not print the token.</li>
<li>Check later with <code>uvx verae-mcp-timestamping-server session</code>.</li>
</ol>
<p>If you start the server before registering, it prints those same steps on its error stream and still starts. Global stamps need the file. Local stamps do not.</p>
</section>
<section id="clients">
<h2 class="section">Connect an assistant</h2>
<p class="section-note">Pick the environment you use. The config names the program. It does not contain the token.</p>
<div class="grid">
<article class="card">
<p class="step">Claude Desktop</p>
<h2>macOS and Windows</h2>
<p>macOS file: <span class="path">~/Library/Application Support/Claude/claude_desktop_config.json</span>. Windows file: <span class="path">%APPDATA%\Claude\claude_desktop_config.json</span>. Restart Claude after saving.</p>
<p>macOS: <span class="path">~/Library/Application Support/Claude/claude_desktop_config.json</span>. Windows: <span class="path">%APPDATA%\Claude\claude_desktop_config.json</span>.</p>
</article>
<article class="card">
<p class="step">Cursor</p>
<h2>Project or user</h2>
<p>User file: <span class="path">~/.cursor/mcp.json</span>. A project file is <span class="path">.cursor/mcp.json</span> in that project. Restart Cursor after saving.</p>
<p>User file: <span class="path">~/.cursor/mcp.json</span>. Project file: <span class="path">.cursor/mcp.json</span>.</p>
</article>
<article class="card">
<p class="step">VS Code and others</p>
<h2>mcp.json</h2>
<p>The same JSON works for any client that reads the Model Context Protocol <code>mcpServers</code> map, including VS Code with an MCP extension.</p>
<p class="step">Local uvx</p>
<h2>Run on this computer</h2>
<p>Use <code>command: uvx</code> / <code>args: ["verae-mcp-timestamping-server"]</code> instead of a remote URL when you want the process on your machine.</p>
</article>
</div>
<h2 class="section">Run it on your computer</h2>
<p class="section-note">The assistant starts the server. Your login in <span class="path">~/.verae/auth.properties</span> is the one it uses.</p>
<pre>{
"mcpServers": {
"verae-timestamping": {
"command": "uvx",
"args": ["verae-mcp-timestamping-server"]
}
}
}</pre>
<h2 class="section">Use the public server</h2>
<p class="section-note">No local program. The assistant connects to this host. When per-user sign-in is on, the first connection opens a browser page at <a href="/account">/account</a> to create an account or log in. That token stays in the assistant’s OAuth store, not in this file.</p>
<pre>{
"mcpServers": {
"verae-timestamping": {
"url": "https://mcp.veraetime.net/mcp"
}
}
}</pre>
</section>
<section id="token">
<h2 class="section">Where the token lives</h2>
<h2 class="section">Where credentials live</h2>
<div class="grid">
<article class="card">
<p class="step">Your machine</p>
<h2>~/.verae/auth.properties</h2>
<p>Mode 600, owned by you. Holds the JWT, its expiry, your username, and your password so a stamp can log in again before the 24-hour token expires. Do not commit this file.</p>
<p>Mode 600. Holds the JWT for local/global stamps. Do not commit this file.</p>
</article>
<article class="card">
<p class="step">Your assistant</p>
<h2>OAuth file</h2>
<p>A browser login on the public server gives the assistant a VeraeTime JWT and a refresh token. The Python client stores that under <span class="path">~/.verae/oauth/mcp-tokens.json</span>, also mode 600.</p>
<h2>OAuth store</h2>
<p>Browser login on the public server keeps tokens in the assistant’s OAuth store, not in mcp.json.</p>
</article>
<article class="card">
<p class="step">Never</p>
<h2>Chat or mcp.json</h2>
<p>Do not paste the token into a conversation or into the client config. <code>session</code> shows the username and expiry if you need to confirm the login.</p>
<p>Do not paste tokens into a conversation or client config.</p>
</article>
</div>
</section>
@ -212,8 +272,213 @@
<footer>
<div class="wrap">
<span>Verae timestamping · <a href="https://www.verae.com/">verae.com</a></span>
<span><a href="sample.html">Try a stamp</a> · <a href="https://api.veraetime.net/docs/swagger/">API docs</a></span>
<span><a href="sample.html">Try a stamp</a> · <a href="/hasher/latest.json">hasher latest</a> · <a href="https://api.veraetime.net/docs/swagger/">API docs</a></span>
</div>
</footer>
<script>
(function () {
const HASHER_BASE = (location.origin || "https://mcp.georgelambert.org") + "/hasher";
// On veraetime host, hasher artifacts still live on georgelambert CDN-style path.
const ARTIFACT_BASES = [
HASHER_BASE,
"https://mcp.georgelambert.org/hasher"
];
const host = location.hostname || "mcp.georgelambert.org";
const mcpUrl = "https://" + host + "/mcp";
document.getElementById("mcp-config").textContent =
`{
"mcpServers": {
"verae-timestamping": {
"url": "${mcpUrl}"
}
}
}`;
document.getElementById("mcp-url-primary").textContent = mcpUrl;
function detectPlatform() {
const uaData = navigator.userAgentData;
let os = null, arch = null;
if (uaData && uaData.platform) {
const p = uaData.platform.toLowerCase();
if (p.includes("mac")) os = "darwin";
else if (p.includes("win")) os = "windows";
else if (p.includes("linux") || p.includes("chrome os")) os = "linux";
else if (p.includes("android")) os = "android";
else if (p.includes("ios") || p.includes("iphone") || p.includes("ipad")) os = "ios";
}
const ua = navigator.userAgent || "";
const plat = (navigator.platform || "").toLowerCase();
if (!os) {
if (/android/i.test(ua)) os = "android";
else if (/iphone|ipad|ipod/i.test(ua)) os = "ios";
else if (/mac/i.test(plat) || /mac os/i.test(ua)) os = "darwin";
else if (/win/i.test(plat) || /windows/i.test(ua)) os = "windows";
else if (/linux/i.test(plat) || /linux/i.test(ua)) os = "linux";
}
// arch hints
if (uaData && Array.isArray(uaData.architecture)) {
// not standard; ignore
}
if (/aarch64|arm64/i.test(ua) || /arm64/i.test(plat)) arch = "aarch64";
else if (/x86_64|win64|wow64|amd64/i.test(ua) || /x86_64|macintel/i.test(plat)) arch = "x86_64";
// Apple Silicon often reports MacIntel in platform historically; prefer arm on modern Macs when possible
if (os === "darwin" && !arch) {
// Safari on Apple Silicon still often says MacIntel; default arm64 for darwin when unknown in 2026
arch = "aarch64";
}
if (os === "windows" && !arch) arch = "x86_64";
if (os === "linux" && !arch) arch = "x86_64";
let key = null;
let label = "unknown";
let kind = "wasm";
if (os === "darwin" && arch === "aarch64") { key = "aarch64-apple-darwin"; label = "macOS Apple Silicon"; kind = "native"; }
else if (os === "darwin" && arch === "x86_64") { key = "x86_64-apple-darwin"; label = "macOS Intel"; kind = "native"; }
else if (os === "linux" && arch === "x86_64") { key = "x86_64-unknown-linux-gnu"; label = "Linux x86_64"; kind = "native"; }
else if (os === "linux" && arch === "aarch64") { key = "aarch64-unknown-linux-gnu"; label = "Linux aarch64"; kind = "native"; }
else if (os === "windows" && arch === "x86_64") { key = "x86_64-pc-windows-gnu"; label = "Windows x86_64"; kind = "native"; }
else if (os === "android" || os === "ios") { label = os === "ios" ? "iOS / iPadOS" : "Android"; kind = "wasm"; }
else { label = (os || "browser") + (arch ? (" / " + arch) : ""); kind = "wasm"; }
return { os, arch, key, label, kind };
}
function fmtSize(n) {
if (n < 1024) return n + " B";
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
return (n / (1024 * 1024)).toFixed(2) + " MB";
}
async function fetchJson(urls) {
let lastErr;
for (const url of urls) {
try {
const res = await fetch(url, { cache: "no-cache" });
if (!res.ok) throw new Error(res.status + " " + url);
return { data: await res.json(), url };
} catch (e) { lastErr = e; }
}
throw lastErr || new Error("fetch failed");
}
function installSnippet(fileUrl, key, isWindows) {
if (isWindows) {
return `# PowerShell
Invoke-WebRequest -Uri "${fileUrl}" -OutFile verae-sha256.exe
Get-FileHash .\\verae-sha256.exe -Algorithm SHA256
.\\verae-sha256.exe .\\document.pdf`;
}
const name = "verae-sha256";
return `# curl
curl -fsSL -o ${name} "${fileUrl}"
chmod +x ${name}
shasum -a 256 ${name} # or: sha256sum ${name}
./${name} ./document.pdf
# wget
wget -O ${name} "${fileUrl}"
chmod +x ${name}
./${name} ./document.pdf`;
}
async function run() {
const status = document.getElementById("hasher-status");
const actions = document.getElementById("hasher-actions");
const meta = document.getElementById("hasher-meta");
const snippet = document.getElementById("hasher-snippet");
const det = detectPlatform();
let latest, manifest, baseUsed;
try {
const latestRes = await fetchJson(ARTIFACT_BASES.map(b => b + "/latest.json"));
latest = latestRes.data;
// Prefer manifest URL from latest, else construct
const manifestUrls = [];
if (latest.manifest) manifestUrls.push(latest.manifest);
for (const b of ARTIFACT_BASES) {
manifestUrls.push(b + "/" + latest.version + "/manifest.json");
}
const manRes = await fetchJson([...new Set(manifestUrls)]);
manifest = manRes.data;
baseUsed = (manifest.base_url || "").replace(/\/$/, "");
} catch (err) {
status.innerHTML = "Could not load hasher manifest: <code>" + String(err) + "</code>";
return;
}
document.getElementById("manifest-link").href = latest.manifest || (baseUsed + "/manifest.json");
document.getElementById("manifest-link").textContent = latest.manifest || (baseUsed + "/manifest.json");
// fill table
const tbody = document.querySelector("#artifact-table tbody");
tbody.innerHTML = "";
const arts = manifest.artifacts || {};
Object.keys(arts).sort().forEach(k => {
const a = arts[k];
const tr = document.createElement("tr");
const url = baseUsed + "/" + a.file;
tr.innerHTML = "<td><code>" + k + "</code></td><td><a href=\"" + url + "\">" + a.file + "</a></td><td>" + fmtSize(a.size) + "</td><td><code style=\"font-size:11px\">" + a.sha256.slice(0, 16) + "…</code></td>";
tbody.appendChild(tr);
});
if (manifest.wasm) {
const tr = document.createElement("tr");
tr.innerHTML = "<td><code>wasm32</code> (browser/mobile)</td><td><a href=\"" + baseUsed + "/" + manifest.wasm.module + "\">" + manifest.wasm.module + "</a> · <a href=\"" + baseUsed + "/" + manifest.wasm.js + "\">" + manifest.wasm.js + "</a></td><td>—</td><td><code style=\"font-size:11px\">" + (manifest.wasm.sha256 || "").slice(0, 16) + "…</code></td>";
tbody.appendChild(tr);
}
const art = det.key ? arts[det.key] : null;
if (det.kind === "native" && art) {
const fileUrl = baseUsed + "/" + art.file;
const isWin = det.key.indexOf("windows") !== -1;
status.innerHTML = "Detected <strong>" + det.label + "</strong> → <code>" + det.key + "</code> · hasher <strong>v" + manifest.version + "</strong>";
const a = document.createElement("a");
a.className = "btn";
a.href = fileUrl;
a.textContent = "Download " + art.file;
actions.appendChild(a);
const copy = document.createElement("button");
copy.className = "btn secondary";
copy.type = "button";
copy.textContent = "Show install commands";
copy.onclick = () => {
snippet.hidden = false;
snippet.textContent = installSnippet(fileUrl, det.key, isWin);
};
actions.appendChild(copy);
meta.innerHTML = "SHA-256 <code>" + art.sha256 + "</code> · " + fmtSize(art.size) + " · verify against <a href=\"" + baseUsed + "/SHA256SUMS\">SHA256SUMS</a>";
snippet.hidden = false;
snippet.textContent = installSnippet(fileUrl, det.key, isWin);
} else {
status.innerHTML = "Detected <strong>" + det.label + "</strong>. No native binary for this environment — use the <strong>WASM/JS</strong> hasher (browser/mobile).";
if (manifest.wasm) {
const wa = document.createElement("a");
wa.className = "btn";
wa.href = baseUsed + "/" + manifest.wasm.module;
wa.textContent = "Download " + manifest.wasm.module;
actions.appendChild(wa);
const ja = document.createElement("a");
ja.className = "btn secondary";
ja.href = baseUsed + "/" + manifest.wasm.js;
ja.textContent = manifest.wasm.js;
actions.appendChild(ja);
const tryBtn = document.createElement("a");
tryBtn.className = "btn secondary";
tryBtn.href = "sample.html";
tryBtn.textContent = "Open stamp sample";
actions.appendChild(tryBtn);
meta.innerHTML = "WASM SHA-256 <code>" + manifest.wasm.sha256 + "</code>. Import <code>" + manifest.wasm.js + "</code> in a page or worker; hash bytes locally, then call MCP <code>stamp</code> with the hex digest only.";
snippet.hidden = false;
snippet.textContent =
`import init, { sha256_hex_js, Sha256Stream } from "${baseUsed}/${manifest.wasm.js}";
await init();
const digest = sha256_hex_js(new TextEncoder().encode("hello"));
// or stream a File via Sha256Stream.update(chunk) / .finish()
// then POST stamp { sha256: digest } to ${mcpUrl}`;
}
}
}
run();
})();
</script>
</body>
</html>

View file

@ -16,10 +16,10 @@
<body>
<h1>Verae timestamping MCP sample</h1>
<p>This page calls a <strong>Model Context Protocol</strong> (MCP) server. The three letters are MCP. The server name is <code>verae-timestamping</code>. It stamps a SHA-256 digest. It does not upload a file.</p>
<p>Start the server on this machine, then submit. A page on the public internet cannot reach <code>127.0.0.1</code>.</p>
<p>This page defaults to the public MCP on this host. You can also point it at a local <code>uvx</code> server on <code>127.0.0.1</code>.</p>
<pre>VERAE_MCP_CORS=1 VERAE_MCP_TRANSPORT=streamable-http uv run verae-mcp-timestamping-server</pre>
<form id="stamp-form">
<label>Server URL <input id="url" value="http://127.0.0.1:8000/mcp"></label>
<label>Server URL <input id="url" value="https://mcp.georgelambert.org/mcp" data-default-remote="1"></label>
<label>SHA-256 <input id="sha" value="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" spellcheck="false"></label>
<label>Scope
<select id="scope">
@ -87,5 +87,16 @@ document.getElementById("stamp-form").addEventListener("submit", async (ev) => {
}
});
</script>
<script>
(function(){
var el = document.getElementById("url");
if (!el) return;
var host = location.hostname || "mcp.georgelambert.org";
if (host && host !== "localhost" && host !== "127.0.0.1") {
el.value = "https://" + host + "/mcp";
}
})();
</script>
</body>
</html>