verae-mcp-timestamping-server/site/index.html
George Lambert 1ca834900e feat: site WIP — downloads, explorer, hasher, TestFlight enroll
Commit restored mcp.verae.ai site pages (app, register, macos, windows,
explorer), hasher/download artifacts, index/sample/hasher updates, ledger
and global_client improvements, NATS stamp helper, and TestFlight enroll
service. Ignore debug-trace/ and editor backups. Standard Readme for
download and enroll docs.
2026-09-29 11:18:59 -04:00

745 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Install Verae timestamping</title>
<meta name="description" content="Verae MCP timestamping: works with or without AI. Embed in web apps, call from local programs, or connect Claude/ChatGPT/Grok. Hash locally; stamp digests only.">
<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">
<link rel="icon" href="assets/VeraeFullLogo.png">
<style>
:root {
--ink: oklch(0.21 0.020 310);
--ink-soft: oklch(0.42 0.018 310);
--ink-mute: oklch(0.60 0.014 310);
--paper: oklch(0.992 0.004 90);
--soft: oklch(0.972 0.008 85);
--hair: oklch(0.90 0.010 80);
--purple: oklch(0.32 0.110 310);
--purple-mid: oklch(0.42 0.130 312);
--purple-wash: oklch(0.96 0.020 312);
--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; }
body {
margin: 0;
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--ink);
background: var(--paper);
line-height: 1.55;
}
a { color: var(--purple); }
header {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1100px;
margin: 0 auto;
padding: 28px 28px 0;
}
.brand img { height: 46px; width: auto; display: block; }
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, nav a.active { color: var(--purple); }
.hero {
max-width: 1100px;
margin: 0 auto;
padding: 64px 28px 24px;
}
.eyebrow {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--purple-mid);
}
h1 {
font-family: Fraunces, Georgia, serif;
font-weight: 560;
font-size: clamp(40px, 6vw, 68px);
line-height: 1.05;
letter-spacing: -0.03em;
margin: 12px 0 18px;
max-width: 18ch;
}
.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 {
background: white;
border: 1px solid var(--hair);
border-radius: 16px;
padding: 22px 22px 18px;
}
.card h2 { font-size: 18px; margin: 0 0 8px; }
.card p { margin: 0; color: var(--ink-soft); font-size: 15px; }
.step { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--purple-mid); }
h2.section {
font-family: Fraunces, Georgia, serif;
font-size: 34px;
font-weight: 560;
margin: 56px 0 8px;
}
.section-note { color: var(--ink-soft); max-width: 44rem; margin-top: 0; }
pre {
background: var(--dark);
color: oklch(0.95 0.01 90);
border-radius: 14px;
padding: 18px 20px;
overflow: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13.5px;
line-height: 1.5;
}
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; }
.path { background: var(--purple-wash); padding: 2px 6px; border-radius: 6px; }
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;
color: var(--ink-mute);
font-size: 14px;
}
footer .wrap { padding: 0; display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.connectors { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 18px; margin-top: 28px; }
.connector {
background: white;
border: 1px solid var(--hair);
border-radius: 16px;
padding: 22px 22px 18px;
display: flex;
flex-direction: column;
gap: 8px;
}
.connector h3 { font-size: 18px; margin: 0; }
.connector .tag {
display: inline-block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--purple-mid);
background: var(--purple-wash);
padding: 3px 8px;
border-radius: 999px;
width: fit-content;
}
.connector .tag.warn { color: oklch(0.45 0.10 70); background: oklch(0.96 0.04 85); }
.connector ol { margin: 6px 0 0; padding-left: 1.15rem; color: var(--ink-soft); font-size: 14.5px; }
.connector ol li { margin: 0.4rem 0; }
.connector .urlbox {
margin-top: 8px;
background: var(--soft);
border-radius: 10px;
padding: 10px 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12.5px;
word-break: break-all;
}
.connector .footnote { font-size: 13px; color: var(--ink-mute); margin: 6px 0 0; }
@media (max-width: 800px) {
.grid { grid-template-columns: 1fr; }
header { padding-top: 18px; }
nav { gap: 14px; }
}
</style>
</head>
<body>
<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="ways.html">Ways to use</a>
<a href="developers.html">Developers</a>
<a href="sample.html">Try a stamp</a>
</nav>
</header>
<main>
<section class="hero">
<p class="eyebrow">Model Context Protocol · not AI-only</p>
<h1>Hash locally. Stamp remotely.</h1>
<p class="lede">This MCP is a normal remote API. It runs <strong>outside any AI</strong>: embed it in a web page, call it from a local program or service, or optionally wire it into Claude / ChatGPT / Grok. Download a small <code>verae-sha256</code> binary (or use WASM in the browser). Digests go to the server; file bytes stay on your machine.</p>
<hr class="gold">
<p class="lede" style="margin-top:22px">See <a href="#ways"><strong>Ways to use</strong></a> for AI MCP, browser, macOS direct download / TestFlight / Windows GUI / explorer, CLI, JS, and curl. New: <a href="windows.html"><strong>Windows GUI + CLI</strong></a> (amd64 EXEs with screenshots &amp; checksums), <a href="app.html"><strong>full browser app</strong></a> (Register + Verify, auth defaults, green/red status), and <a href="register.html"><strong>multi-document registration</strong></a> — drop many files, check global timestamps, export CSV / JSON / text / official PDF certificates with QR lookup.</p>
</section>
<div class="wrap">
<section id="ways">
<h2 class="section">Ways to use Verae timestamps</h2>
<p class="section-note">Same public MCP for every path. Hash on the device; only digests are sent. Start with <strong>AI MCP</strong>, then pick a local or browser client. Full detail — including <strong>macOS direct download</strong>, explorer, and TestFlight — on the <a href="ways.html">Ways to use</a> hub.</p>
<ol class="steps" style="margin-top:14px">
<li><strong>AI MCP</strong> — Claude.ai, ChatGPT, Grok, Claude Desktop, or Cursor → <code class="mcp-endpoint-label">https://mcp.verae.ai/mcp</code> (<a href="#connectors">Connectors</a>).</li>
<li><strong>Browser</strong> — <a href="app.html">Browser app</a>, <a href="register.html">Register docs</a>, <a href="sample.html">Sample</a>.</li>
<li><strong>UI</strong> — <a href="macos.html">macOS notarized zip</a> (no TestFlight) · <a href="ways.html#testflight">TestFlight</a> · <a href="windows.html">Windows GUI</a> · <a href="explorer.html">Explorer</a>.</li>
<li><strong>CLI</strong> — <code>uvx verae-mcp-timestamping-client</code> or Windows <code>verae-timestamp.exe</code> (<a href="#apps">without AI</a> · <a href="windows.html#cli">Windows CLI</a>).</li>
<li><strong>JavaScript</strong> — browser <code>fetch</code> to <code>/mcp</code> (<a href="#apps">embed snippet</a>).</li>
<li><strong>curl</strong> — raw JSON-RPC (<a href="#apps">curl examples</a>).</li>
<li><strong>Also</strong> — local <a href="#hasher">hasher</a> / <a href="hasher.html">WASM hasher</a>, optional <a href="#register">machine login</a>.</li>
</ol>
<div class="btn-row" style="margin-top:16px">
<a class="btn" href="ways.html">Ways to use hub</a>
<a class="btn secondary" href="macos.html">macOS download</a>
<a class="btn secondary" href="explorer.html">Explorer</a>
<a class="btn secondary" href="ways.html#testflight">Request Mac TestFlight</a>
<a class="btn secondary" href="windows.html">Windows downloads</a>
<a class="btn secondary" href="#connectors">AI connectors</a>
</div>
</section>
<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="windows">
<h2 class="section">Windows desktop</h2>
<p class="section-note">Frozen amd64 EXEs — GUI (PySide6), CLI, and optional local hasher. SmartScreen will warn (not code-signed); verify SHA-256 before running. Digests only; bytes never upload.</p>
<div class="btn-row" style="margin-top:16px">
<a class="btn" href="windows.html">Windows download page</a>
<a class="btn secondary" href="/downloads/windows/verae-timestamp-gui.exe">GUI EXE</a>
<a class="btn secondary" href="/downloads/windows/verae-timestamp.exe">CLI EXE</a>
<a class="btn secondary" href="/downloads/windows/SHA256SUMS">SHA256SUMS</a>
</div>
<p class="meta">Stable URLs: <code>/downloads/windows/</code> · instructions, screenshots, and real register/verify sample output on the <a href="windows.html">Windows page</a>.</p>
</section>
<section id="connect">
<h2 class="section">Connect an MCP client</h2>
<p class="section-note">Point any MCP-capable host at this public URL — an AI assistant <em>or</em> your own app. You do not need to run a local server. For embedding in web pages and local programs (no AI), see <a href="#apps">Use without AI</a>. Browser assistants (Claude.ai, ChatGPT, Grok) use Connectors below.</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="connectors">
<h2 class="section">Connectors by host</h2>
<p class="section-note">MCP endpoint for this page’s host is filled in automatically below. First connection may open a browser login; the assistant keeps the token in its OAuth store — never paste tokens into chat or <code>mcp.json</code>.</p>
<div class="connectors">
<article class="connector">
<span class="tag">Browser</span>
<h3>Claude.ai</h3>
<ol>
<li>Open <strong>Customize → Connectors</strong> (Team/Enterprise Owners: Organization settings → Connectors).</li>
<li>Click <strong>Add custom connector</strong> (or Add → Custom → Web).</li>
<li>Paste the MCP server URL. Leave Advanced OAuth blank unless you registered a fixed client.</li>
<li>Click <strong>Add</strong>, then <strong>Connect</strong> and finish the Verae browser login.</li>
</ol>
<div class="urlbox mcp-endpoint">https://mcp.verae.ai/mcp</div>
<p class="footnote">Docs: <a href="https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp">Claude Help — remote MCP</a></p>
</article>
<article class="connector">
<span class="tag">Browser</span>
<h3>ChatGPT</h3>
<ol>
<li>Avatar → <strong>Settings → Connectors</strong>.</li>
<li>Under Advanced, enable <strong>Developer Mode</strong> (Pro / Team / Enterprise / Edu).</li>
<li><strong>Add custom connector</strong>: name <code>Verae timestamping</code>, URL below, Authentication <strong>OAuth</strong>.</li>
<li>Create, complete the Verae login, then enable the connector in a chat and confirm tool calls.</li>
</ol>
<div class="urlbox mcp-endpoint">https://mcp.verae.ai/mcp</div>
<p class="footnote">Needs a public Streamable HTTP <code>/mcp</code> URL. ChatGPT OAuth redirect: <code>https://chatgpt.com/connector_platform_oauth_redirect</code>.</p>
</article>
<article class="connector">
<span class="tag">Browser</span>
<h3>Grok (grok.com)</h3>
<ol>
<li>Go to <a href="https://grok.com/connectors">grok.com/connectors</a>.</li>
<li>Click <strong>New Connector</strong> → <strong>Custom</strong>.</li>
<li>Enter the MCP server URL and complete any OAuth/login prompts.</li>
<li>Business/Enterprise: a team admin provisions it first in <a href="https://console.x.ai">console.x.ai</a> (Grok Business → Connectors).</li>
</ol>
<div class="urlbox mcp-endpoint">https://mcp.verae.ai/mcp</div>
<p class="footnote">Must be public HTTPS. Docs: <a href="https://docs.x.ai/grok/connectors">xAI — Connectors</a></p>
</article>
<article class="connector">
<span class="tag">Desktop</span>
<h3>Claude Desktop</h3>
<ol>
<li>Edit the config file for your OS (paths below).</li>
<li>Add the <code>mcpServers</code> JSON from <a href="#connect">Connect</a> (remote <code>url</code>).</li>
<li>Restart Claude Desktop; approve Verae browser login on first use.</li>
</ol>
<div class="urlbox">macOS: ~/Library/Application Support/Claude/claude_desktop_config.json<br>Windows: %APPDATA%\Claude\claude_desktop_config.json</div>
</article>
<article class="connector">
<span class="tag">IDE</span>
<h3>Cursor</h3>
<ol>
<li>Open user <span class="path">~/.cursor/mcp.json</span> or project <span class="path">.cursor/mcp.json</span>.</li>
<li>Add the remote <code>url</code> entry from <a href="#connect">Connect</a>.</li>
<li>Reload MCP in Cursor; complete browser login if prompted.</li>
</ol>
<div class="urlbox mcp-endpoint">https://mcp.verae.ai/mcp</div>
</article>
<article class="connector">
<span class="tag">Local</span>
<h3>uvx on this machine</h3>
<ol>
<li>Prefer the remote public URL when you can.</li>
<li>For a local process: <code>command: uvx</code>, <code>args: ["verae-mcp-timestamping-server"]</code>.</li>
<li>Optional account: <code>uvx verae-mcp-timestamping-server setup</code> → <span class="path">~/.verae/auth.properties</span> (mode 600).</li>
</ol>
<p class="footnote">Local stdio works for Claude Desktop / Cursor on the same computer — not for ChatGPT or grok.com (those need the public URL).</p>
</article>
</div>
</section>
<section id="apps">
<h2 class="section">Use without AI — web apps &amp; local processes</h2>
<p class="section-note"><strong>No assistant required.</strong> The same public MCP endpoint is a plain Streamable HTTP JSON-RPC service. Embed it in a web application (browser <code>fetch</code>), call it from a desktop/CLI process, a backend worker, or a mobile app. AI connectors are optional. Endpoint: <code class="mcp-endpoint-label">https://mcp.verae.ai/mcp</code>. Hash on the device; send only the hex digest. Never upload file bytes.</p>
<div class="grid" style="margin-top:18px;margin-bottom:8px">
<article class="card">
<h2>Web page / SPA</h2>
<p>Hash with WASM in the browser, then <code>fetch</code> <code>/mcp</code> (CORS <code>*</code>). Live: <a href="app.html">app.html</a>, <a href="register.html">register.html</a>, and <a href="sample.html">sample.html</a>.</p>
</article>
<article class="card">
<h2>Local program</h2>
<p>CLI, daemon, or any language with HTTP: official Python client, curl JSON-RPC, or an MCP SDK. Runs on your machine with no chat UI.</p>
</article>
<article class="card">
<h2>Optional: AI hosts</h2>
<p>Same URL works in Claude.ai, ChatGPT, Grok, Claude Desktop, and Cursor when you want an assistant to call <code>stamp</code> / <code>verify</code> for you.</p>
</article>
</div>
<h3 style="margin-top:28px;font-size:20px;">Local process (Python CLI / scripts)</h3>
<p class="section-note">Official client for scripts and desktop tools — no AI involved. Hash locally, stamp via MCP, write receipts under <span class="path">~/.verae/receipts/</span>.</p>
<pre id="cli-snippet"># install hasher (or let the client auto-fetch)
DIGEST=$(verae-sha256 ./document.pdf)
# stamp against the public server (digest only — file stays local)
uvx verae-mcp-timestamping-client \
--url https://mcp.verae.ai/mcp \
--scope global \
./document.pdf
# hash only (no network)
uvx verae-mcp-timestamping-client --hash-only ./document.pdf</pre>
<p class="meta">Optional machine OAuth: set <code>VERAE_OAUTH_CLIENT_ID</code> / <code>VERAE_OAUTH_CLIENT_SECRET</code> (and usually <code>VERAE_OAUTH_ISSUER</code>). Tokens land under <span class="path">~/.verae/oauth/</span>, not in chat.</p>
<h3 style="margin-top:36px;font-size:20px;">Any language (MCP SDK or raw JSON-RPC)</h3>
<p class="section-note">Transport is <strong>Streamable HTTP</strong> at <code>/mcp</code>. Session id comes back in the <code>Mcp-Session-Id</code> response header — send it on later requests. Tools: <code>stamp</code> (create), <code>hash_report</code> (lookup by digest), <code>verify</code> (check one receipt id), plus account helpers.</p>
<h3 style="margin-top:36px;font-size:20px;">Lookup receipts for an existing hash</h3>
<p class="section-note"><code>hash_report</code> returns the JSON receipt history for one SHA-256 (Carfax-style). Optional <code>want_scope=global</code> asks api.veraetime.net as the central source of truth. <code>verify</code> checks a single <code>receipt_id</code> from a prior stamp.</p>
<pre id="lookup-snippet"># after initialize + notifications/initialized (same session as above):
curl -sS -X POST https://mcp.verae.ai/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: SESSION' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hash_report","arguments":{"sha256":"DIGEST_HEX","want_scope":"global"}}}'
# verify one receipt id from a previous stamp:
curl -sS -X POST https://mcp.verae.ai/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: SESSION' \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"verify","arguments":{"receipt_id":"RECEIPT_ID","want_scope":"global"}}}'</pre>
<p class="meta">In the browser sample, pick tool <code>hash_report</code> or <code>verify</code>. In JS, use the same <code>tools/call</code> shape with <code>name: "hash_report"</code> and <code>{ sha256, want_scope }</code>.</p>
<pre id="rpc-snippet"># 1) initialize
curl -sS -D - -X POST https://mcp.verae.ai/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0.0"}}}'
# 2) notifications/initialized (include Mcp-Session-Id from step 1)
# 3) tools/call stamp
curl -sS -X POST https://mcp.verae.ai/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: SESSION' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"stamp","arguments":{"sha256":"DIGEST_HEX","scope":"global"}}}'</pre>
<p class="meta">Python SDK shape (same as the official client): <code>Client(url)</code> then <code>await client.call_tool("stamp", {"sha256": digest, "scope": "global"})</code>.</p>
<h3 style="margin-top:36px;font-size:20px;">Embed in a web page (browser JavaScript)</h3>
<p class="section-note">Drop this into any web app — not an AI product. Same-origin pages on this host (and CORS <code>*</code> on <code>/mcp</code>) can <code>fetch</code> the endpoint. Live demos: <a href="app.html">full browser app</a>, <a href="register.html">multi-doc registration</a>, <a href="sample.html">raw MCP sample</a>. Hash in-page with WASM (or Web Crypto), then stamp the digest only.</p>
<pre id="js-snippet">const url = "https://mcp.verae.ai/mcp";
async function post(body, session) {
const headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream"
};
if (session) headers["Mcp-Session-Id"] = session;
const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
const text = await res.text();
const sessionId = res.headers.get("mcp-session-id") || session;
let parsed; try { parsed = JSON.parse(text); } catch { parsed = text; }
if (!res.ok) throw new Error(res.status + " " + text);
return { parsed, sessionId };
}
const init = await post({
jsonrpc: "2.0", id: 1, method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "browser-app", version: "0.1.0" }
}
});
await post({ jsonrpc: "2.0", method: "notifications/initialized" }, init.sessionId);
const call = await post({
jsonrpc: "2.0", id: 2, method: "tools/call",
params: { name: "stamp", arguments: { sha256: DIGEST_HEX, scope: "global" } }
}, init.sessionId);
console.log(call.parsed);</pre>
<p class="meta">For hashing in the browser, use the WASM module from <a href="#hasher">Get the local hasher</a> (<code>sha256_hex_js</code> / <code>Sha256Stream</code>). Do not <code>POST</code> file contents to <code>/mcp</code>.</p>
</section>
<section id="register">
<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>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>
</section>
<section id="token">
<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. 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 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 tokens into a conversation or client config.</p>
</article>
</div>
</section>
</div>
</main>
<footer>
<div class="wrap">
<span>Verae timestamping · <a href="https://www.verae.com/">verae.com</a></span>
<span><a href="ways.html">Ways to use</a> · <a href="explorer.html">Explorer</a> · <a href="macos.html">macOS</a> · <a href="ways.html#testflight">TestFlight</a> · <a href="windows.html">Windows</a> · <a href="app.html">Browser app</a> · <a href="register.html">Register docs</a> · <a href="sample.html">Sample</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;
document.querySelectorAll(".mcp-endpoint").forEach(function (el) { el.textContent = mcpUrl; });
document.querySelectorAll(".mcp-endpoint-label").forEach(function (el) { el.textContent = mcpUrl; });
(function () {
var cli = document.getElementById("cli-snippet");
if (cli) cli.textContent = cli.textContent.replaceAll("https://mcp.verae.ai/mcp", mcpUrl);
var rpc = document.getElementById("rpc-snippet");
if (rpc) rpc.textContent = rpc.textContent.replaceAll("https://mcp.verae.ai/mcp", mcpUrl);
var js = document.getElementById("js-snippet");
if (js) js.textContent = js.textContent.replaceAll("https://mcp.verae.ai/mcp", mcpUrl);
var lu = document.getElementById("lookup-snippet");
if (lu) lu.textContent = lu.textContent.replaceAll("https://mcp.verae.ai/mcp", 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|windows arm/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";
// Optional URL override: ?libc=musl|gnu on Linux
const params = new URLSearchParams(location.search);
const libcPref = (params.get("libc") || "").toLowerCase();
const wantMusl = libcPref === "musl";
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 = wantMusl ? "x86_64-unknown-linux-musl" : "x86_64-unknown-linux-gnu";
label = wantMusl ? "Linux x86_64 (musl/static)" : "Linux x86_64 (glibc)";
kind = "native";
}
else if (os === "linux" && arch === "aarch64") {
key = wantMusl ? "aarch64-unknown-linux-musl" : "aarch64-unknown-linux-gnu";
label = wantMusl ? "Linux aarch64 (musl/static)" : "Linux aarch64 (glibc)";
kind = "native";
}
else if (os === "windows" && arch === "x86_64") { key = "x86_64-pc-windows-gnu"; label = "Windows x86_64"; kind = "native"; }
else if (os === "windows" && arch === "aarch64") { key = "aarch64-pc-windows-gnullvm"; label = "Windows ARM64"; 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";
// prefer multi-doc page when present
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>