notion-mcp-server
Version:
Notion MCP server with token auth: connect Claude Code, Claude Desktop, Cursor, VS Code, Codex and any MCP client to Notion. Search, create and edit pages, query databases, comment and upload files. Runs headless in CI and agents.
69 lines (68 loc) • 2.4 kB
JavaScript
import { createHash } from "node:crypto";
import { stableStringify } from "../utils/stable-json.js";
const TTL_MS = 5 * 60 * 1000;
const MAX_ENTRIES = 512;
const cache = new Map();
function evictExpired(now) {
for (const [key, entry] of cache) {
if (entry.state === "completed" && entry.expiresAt <= now)
cache.delete(key);
}
if (cache.size <= MAX_ENTRIES)
return;
// Oldest completed entries first (Map iteration is insertion order, and
// complete() re-inserts).
let overflow = cache.size - MAX_ENTRIES;
for (const [key, entry] of cache) {
if (overflow <= 0)
break;
if (entry.state !== "completed")
continue;
cache.delete(key);
overflow--;
}
}
function fingerprint(operation, items, atomic) {
return createHash("sha256").update(stableStringify({ operation, items, atomic })).digest("hex");
}
export function begin(operation, idempotencyKey, items, atomic) {
evictExpired(Date.now());
const requestFingerprint = fingerprint(operation, items, atomic);
const existing = cache.get(idempotencyKey);
if (!existing) {
cache.set(idempotencyKey, { state: "pending", fingerprint: requestFingerprint });
return {
action: "execute",
key: idempotencyKey,
fingerprint: requestFingerprint,
};
}
if (existing.fingerprint !== requestFingerprint) {
return { action: "conflict" };
}
if (existing.state === "pending") {
return { action: "pending" };
}
return { action: "deduplicated", result: existing.result };
}
function ownsPending(key, requestFingerprint) {
const current = cache.get(key);
return current?.state === "pending" && current.fingerprint === requestFingerprint;
}
export function complete(key, requestFingerprint, result) {
if (!ownsPending(key, requestFingerprint))
return;
cache.delete(key);
cache.set(key, {
state: "completed",
fingerprint: requestFingerprint,
result,
expiresAt: Date.now() + TTL_MS,
});
}
/** Drop a pending entry without caching a result, so the same key can run again. */
export function release(key, requestFingerprint) {
if (ownsPending(key, requestFingerprint))
cache.delete(key);
}
export const _internal = { fingerprint, evictExpired, cache, MAX_ENTRIES, TTL_MS };