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:
parent
c3e0cd3abb
commit
fc3d0866d8
3 changed files with 386 additions and 52 deletions
58
site/hasher.html
Normal file
58
site/hasher.html
Normal 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>
|
||||
365
site/index.html
365
site/index.html
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue