@otskit/mcp 0.6.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,16 +8,16 @@
8
8
  [![npm version](https://img.shields.io/npm/v/@otskit/mcp.svg)](https://www.npmjs.com/package/@otskit/mcp)
9
9
  [![npm downloads](https://img.shields.io/npm/dt/@otskit/mcp.svg)](https://www.npmjs.com/package/@otskit/mcp)
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-6-blue.svg)](https://www.typescriptlang.org/)
11
- [![Node 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
11
+ [![Node >=20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
13
  [![Glama](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP/badges/score.svg)](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP)
14
- [![OTSkit-MCP MCP server](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP/badges/card.svg)](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP)
14
+ [![smithery badge](https://smithery.ai/badge/otskit/otskit-mcp)](https://smithery.ai/servers/otskit/otskit-mcp)
15
15
 
16
- OpenTimestamps MCP server stamp, upgrade, and verify Bitcoin timestamps via AI agents.
16
+ OpenTimestamps MCP server - stamp, upgrade, and verify Bitcoin timestamps via AI agents.
17
17
 
18
- Exposes a set of tools to any MCP-compatible agent so it can timestamp documents, monitor confirmation status, and verify proofs against the Bitcoin blockchain all from a conversation.
18
+ Exposes a set of tools to any MCP-compatible agent so it can timestamp documents, monitor confirmation status, and verify proofs against the Bitcoin blockchain - all from a conversation.
19
19
 
20
- > **Note on confirmation times:** After stamping, a proof is `pending` until Bitcoin confirms it. This typically takes **10–60 minutes** but can take **several hours** during network congestion. Use `ots-mcp watch` or `upgrade_timestamp` to monitor. A pending status is not an error.
20
+ > **Note on confirmation times:** After stamping, a proof is `pending` until Bitcoin confirms it. This typically takes **~60 minutes** but can take **several hours** during network congestion. Use `ots-mcp watch` or `upgrade_timestamp` to monitor. A pending status is not an error.
21
21
 
22
22
  ## Install
23
23
 
@@ -44,7 +44,7 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
44
44
  | `ots-mcp upgrade <id>` | Check if a pending stamp has been confirmed |
45
45
  | `ots-mcp verify <id>` | Verify a stamp against Bitcoin |
46
46
  | `ots-mcp list [status]` | List stamps (`pending` / `confirmed` / `failed`) |
47
- | `ots-mcp watch [minutes]` | Poll pending stamps in real-time (default: 5 min) |
47
+ | `ots-mcp watch [minutes]` | Monitor pending stamps and attempt due upgrades (default: 30 min, minimum: 15 min) |
48
48
  | `ots-mcp check-pending` | Run one upgrade pass over all pending stamps |
49
49
  | `ots-mcp scheduler install\|remove\|status` | Manage OS-level scheduler for auto-upgrades |
50
50
  | `ots-mcp backup [dest]` | Backup the SQLite database |
@@ -56,10 +56,10 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
56
56
  |---|---|
57
57
  | `create_timestamp` | Stamp a SHA-256 hash against 4 public OTS calendars |
58
58
  | `upgrade_timestamp` | Check if a pending stamp has been confirmed in Bitcoin |
59
- | `verify_timestamp` | Verify a stamp proves hash existed before a given Bitcoin block |
59
+ | `verify_timestamp` | Verify a stamp - proves hash existed before a given Bitcoin block |
60
60
  | `inspect_timestamp` | Inspect a stored proof file without network calls |
61
61
  | `list_pending` | List stamps with status, retry count, and filters |
62
- | `watch` | Open a terminal window monitoring pending stamps in real-time |
62
+ | `watch` | Open a terminal window monitoring pending stamps and attempting due upgrades |
63
63
  | `hash_file` | Compute the SHA-256 of a local file and return it as a 64-char hex string (no network calls) |
64
64
  | `stamp_file` | Compute SHA-256 of a local file and stamp it on Bitcoin in one step |
65
65
 
@@ -67,7 +67,7 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
67
67
 
68
68
  All data is stored in `~/.ots-mcp/`:
69
69
 
70
- ```
70
+ ```text
71
71
  ~/.ots-mcp/
72
72
  ots-mcp.db # SQLite database (stamps, proof files)
73
73
  config.json # Optional config overrides
@@ -104,7 +104,7 @@ npm test # run tests
104
104
 
105
105
  ## Dependencies
106
106
 
107
- - [`@otskit/core`](https://github.com/AlexAlves87/otskit-core) OpenTimestamps core logic
108
- - [`@otskit/client`](https://github.com/AlexAlves87/otskit-client) OTS calendar client
109
- - [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) MCP SDK
110
- - `node-sqlite3-wasm` local database (pure WASM, no native compilation)
107
+ - [`@otskit/core`](https://github.com/AlexAlves87/otskit-core) - OpenTimestamps core logic
108
+ - [`@otskit/client`](https://github.com/AlexAlves87/otskit-client) - OTS calendar client
109
+ - [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) - MCP SDK
110
+ - `node-sqlite3-wasm` - local database (pure WASM, no native compilation)
@@ -1,15 +1,138 @@
1
- import {
2
- getDataDir
3
- } from "./chunk-Y3F7WBP6.js";
4
1
  import {
5
2
  writeAtomic
6
3
  } from "./chunk-YFSUDT24.js";
7
4
 
8
- // src/tools/create-timestamp.ts
9
- import { mkdirSync } from "fs";
5
+ // src/config.ts
6
+ import { readFileSync, existsSync, mkdirSync } from "fs";
10
7
  import { join } from "path";
11
- import { randomUUID } from "crypto";
12
- import { OpenTimestampsClient } from "@otskit/client";
8
+ import { homedir } from "os";
9
+ function getDataDir() {
10
+ return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
11
+ }
12
+ var DEFAULTS = {
13
+ stamp_enabled: true,
14
+ preserve_enabled: true,
15
+ preserve_whitelist: [],
16
+ preserve_max_bytes: 104857600,
17
+ preserve_max_files: 1e4,
18
+ scheduler_interval_minutes: 30,
19
+ calendar_timeout_ms: 1e4,
20
+ calendar_max_response_bytes: 1048576,
21
+ retry_max_attempts: 20,
22
+ log_file: join(getDataDir(), "ots-mcp.log"),
23
+ calendars: [
24
+ "https://alice.btc.calendar.opentimestamps.org",
25
+ "https://bob.btc.calendar.opentimestamps.org",
26
+ "https://finney.calendar.eternitywall.com",
27
+ "https://btc.calendar.catallaxy.com"
28
+ ],
29
+ esplora_url: "https://blockstream.info/api"
30
+ };
31
+ function loadConfig() {
32
+ const dir = getDataDir();
33
+ mkdirSync(dir, { recursive: true });
34
+ const configPath = join(dir, "config.json");
35
+ if (!existsSync(configPath)) return { ...DEFAULTS };
36
+ const raw = JSON.parse(readFileSync(configPath, "utf8"));
37
+ return { ...DEFAULTS, ...raw };
38
+ }
39
+
40
+ // src/db/index.ts
41
+ import { createRequire } from "module";
42
+ import { join as join2 } from "path";
43
+ import { mkdirSync as mkdirSync2, statSync } from "fs";
44
+
45
+ // src/db/schema.ts
46
+ function initDb(db) {
47
+ db.exec("PRAGMA busy_timeout = 5000");
48
+ db.exec("PRAGMA foreign_keys = ON");
49
+ runMigrations(db);
50
+ }
51
+ function runMigrations(db) {
52
+ const row = db.get("PRAGMA user_version");
53
+ if (row.user_version < 1) migrateTo1(db);
54
+ }
55
+ function migrateTo1(db) {
56
+ db.exec("BEGIN");
57
+ try {
58
+ db.exec(`
59
+ CREATE TABLE IF NOT EXISTS stamps (
60
+ id TEXT PRIMARY KEY,
61
+ hash TEXT NOT NULL,
62
+ status TEXT NOT NULL,
63
+ created_at TEXT NOT NULL,
64
+ confirmed_at TEXT,
65
+ bitcoin_block INTEGER,
66
+ bitcoin_time TEXT,
67
+ proof_path TEXT,
68
+ archive_path TEXT,
69
+ last_attempt_at TEXT,
70
+ attempt_count INTEGER NOT NULL DEFAULT 0,
71
+ last_error TEXT,
72
+ next_retry_at TEXT,
73
+ metadata TEXT
74
+ );
75
+ CREATE INDEX IF NOT EXISTS idx_stamps_hash ON stamps(hash);
76
+ CREATE INDEX IF NOT EXISTS idx_stamps_status ON stamps(status);
77
+
78
+ CREATE TABLE IF NOT EXISTS operations_log (
79
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
80
+ stamp_id TEXT NOT NULL REFERENCES stamps(id),
81
+ action TEXT NOT NULL,
82
+ result TEXT NOT NULL,
83
+ error_msg TEXT,
84
+ calendar_uri TEXT,
85
+ response_time_ms INTEGER,
86
+ created_at TEXT NOT NULL
87
+ );
88
+ CREATE INDEX IF NOT EXISTS idx_oplog_stamp_id ON operations_log(stamp_id);
89
+ CREATE INDEX IF NOT EXISTS idx_oplog_created ON operations_log(created_at);
90
+ `);
91
+ db.exec("PRAGMA user_version = 1");
92
+ db.exec("COMMIT");
93
+ } catch (e) {
94
+ db.exec("ROLLBACK");
95
+ throw e;
96
+ }
97
+ }
98
+
99
+ // src/db/index.ts
100
+ var _db = null;
101
+ function getDb() {
102
+ if (_db) return _db;
103
+ const _require = createRequire(import.meta.url);
104
+ const { Database } = _require("node-sqlite3-wasm");
105
+ const dir = getDataDir();
106
+ mkdirSync2(dir, { recursive: true });
107
+ _db = new Database(join2(dir, "db.sqlite"));
108
+ initDb(_db);
109
+ reconcileOrphans(_db);
110
+ return _db;
111
+ }
112
+ function backupDb(destPath) {
113
+ const escaped = destPath.replace(/'/g, "''");
114
+ getDb().exec(`VACUUM INTO '${escaped}'`);
115
+ }
116
+ function reconcileOrphans(db) {
117
+ const pending = db.all(
118
+ `SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
119
+ );
120
+ for (const row of pending) {
121
+ try {
122
+ statSync(row.proof_path);
123
+ } catch {
124
+ db.run(
125
+ `UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`,
126
+ ["proof file missing on disk", row.id]
127
+ );
128
+ }
129
+ }
130
+ }
131
+
132
+ // src/tools/upgrade-timestamp.ts
133
+ import { readFileSync as readFileSync2 } from "fs";
134
+ import { OpenTimestampsClient, UpgradeError } from "@otskit/client";
135
+ import { DetachedTimestampFile } from "@otskit/core";
13
136
 
14
137
  // src/db/stamps.ts
15
138
  function insertStamp(db, params) {
@@ -86,50 +209,7 @@ function logOperation(db, params) {
86
209
  );
87
210
  }
88
211
 
89
- // src/tools/create-timestamp.ts
90
- var HEX64 = /^[0-9a-f]{64}$/i;
91
- async function createTimestamp(input, db, config) {
92
- if (!HEX64.test(input.hash)) {
93
- return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
94
- }
95
- const normalizedHash = input.hash.toLowerCase();
96
- const client = new OpenTimestampsClient({
97
- calendars: config.calendars,
98
- resilience: { timeout: config.calendar_timeout_ms }
99
- });
100
- const t0 = Date.now();
101
- let proofBuffer;
102
- try {
103
- proofBuffer = await client.stamp(normalizedHash);
104
- } catch (e) {
105
- return { error: "calendar_error", details: String(e) };
106
- }
107
- const responseTimeMs = Date.now() - t0;
108
- const id = randomUUID();
109
- const proofDir = join(getDataDir(), "proofs");
110
- mkdirSync(proofDir, { recursive: true });
111
- const proofPath = join(proofDir, `${id}.ots`);
112
- try {
113
- writeAtomic(proofPath, proofBuffer);
114
- } catch (e) {
115
- return { error: "storage_error", details: String(e) };
116
- }
117
- const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
118
- logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
119
- return {
120
- id: record.id,
121
- hash: record.hash,
122
- status: "pending",
123
- calendars: config.calendars,
124
- created_at: record.created_at,
125
- proof_path: proofPath
126
- };
127
- }
128
-
129
212
  // src/tools/upgrade-timestamp.ts
130
- import { readFileSync } from "fs";
131
- import { OpenTimestampsClient as OpenTimestampsClient2, UpgradeError } from "@otskit/client";
132
- import { DetachedTimestampFile, StreamDeserializationContext } from "@otskit/core";
133
213
  function collectAttestations(ts) {
134
214
  const atts = [...ts.attestations];
135
215
  for (const branch of ts.branches) {
@@ -139,8 +219,7 @@ function collectAttestations(ts) {
139
219
  }
140
220
  function checkBitcoinConfirmation(bytes) {
141
221
  try {
142
- const ctx = new StreamDeserializationContext(new Uint8Array(bytes));
143
- const dtf = DetachedTimestampFile.deserialize(ctx);
222
+ const dtf = DetachedTimestampFile.deserialize(new Uint8Array(bytes));
144
223
  const attestations = collectAttestations(dtf.timestamp);
145
224
  const bitcoinAtts = attestations.filter((a) => a.kind === "bitcoin");
146
225
  if (bitcoinAtts.length === 0) return { confirmed: false };
@@ -159,8 +238,8 @@ async function upgradeTimestamp(input, db, config) {
159
238
  const record = getStamp(db, input.id);
160
239
  if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
161
240
  if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
162
- const proofBefore = readFileSync(record.proof_path);
163
- const client = new OpenTimestampsClient2({
241
+ const proofBefore = readFileSync2(record.proof_path);
242
+ const client = new OpenTimestampsClient({
164
243
  calendars: config.calendars,
165
244
  resilience: { timeout: config.calendar_timeout_ms }
166
245
  });
@@ -172,6 +251,20 @@ async function upgradeTimestamp(input, db, config) {
172
251
  upgraded = await client.upgrade(proofBefore);
173
252
  } catch (e) {
174
253
  if (e instanceof UpgradeError) {
254
+ const { confirmed: confirmed2, block: block2 } = checkBitcoinConfirmation(proofBefore);
255
+ if (confirmed2 && block2 !== void 0) {
256
+ const bitcoinTime = now;
257
+ updateStampStatus(db, input.id, {
258
+ status: "confirmed",
259
+ bitcoin_block: block2,
260
+ bitcoin_time: bitcoinTime,
261
+ confirmed_at: now,
262
+ last_attempt_at: now,
263
+ attempt_count: newAttemptCount
264
+ });
265
+ logOperation(db, { stamp_id: input.id, action: "upgrade", result: "success" });
266
+ return { id: input.id, status: "confirmed", bitcoin_block: block2, bitcoin_time: bitcoinTime, proof_path: record.proof_path };
267
+ }
175
268
  updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, next_retry_at: next });
176
269
  logOperation(db, { stamp_id: input.id, action: "upgrade", result: "pending" });
177
270
  return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
@@ -200,70 +293,14 @@ async function upgradeTimestamp(input, db, config) {
200
293
  return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
201
294
  }
202
295
 
203
- // src/tools/verify-timestamp.ts
204
- import { readFileSync as readFileSync2 } from "fs";
205
- import { OpenTimestampsClient as OpenTimestampsClient3 } from "@otskit/client";
206
- async function verifyTimestamp(input, db, config) {
207
- const record = getStamp(db, input.id);
208
- if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
209
- if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
210
- let proofBytes;
211
- try {
212
- proofBytes = readFileSync2(record.proof_path);
213
- } catch (e) {
214
- return { error: "storage_error", details: String(e) };
215
- }
216
- const client = new OpenTimestampsClient3({
217
- calendars: config.calendars,
218
- resilience: { timeout: config.calendar_timeout_ms }
219
- });
220
- let result;
221
- try {
222
- result = await client.verify(proofBytes, record.hash);
223
- } catch (e) {
224
- logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
225
- return { status: "network_error", hash: record.hash, details: String(e) };
226
- }
227
- if (!result.valid) {
228
- if (result.error?.includes("No Bitcoin attestation")) {
229
- logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
230
- return { status: "pending", hash: record.hash, calendars: config.calendars };
231
- }
232
- if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
233
- logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
234
- return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
235
- }
236
- logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
237
- return { status: "unknown", hash: record.hash };
238
- }
239
- logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
240
- return {
241
- status: "confirmed",
242
- hash: record.hash,
243
- bitcoin_block: result.blockHeight,
244
- bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
245
- };
246
- }
247
-
248
- // src/tools/list-pending.ts
249
- function toPublic({ attempt_count: _, last_attempt_at: __, next_retry_at: ___, ...rest }) {
250
- return rest;
251
- }
252
- function listPending(input, db, _config) {
253
- const result = listStamps(db, {
254
- status: input.status ?? "pending",
255
- limit: Math.min(input.limit ?? 50, 200),
256
- offset: input.offset ?? 0,
257
- older_than_hours: input.older_than_hours,
258
- due_now: input.due_now
259
- });
260
- return { items: result.items.map(toPublic), total: result.total };
261
- }
262
-
263
296
  export {
297
+ getDataDir,
298
+ loadConfig,
299
+ getDb,
300
+ backupDb,
301
+ insertStamp,
264
302
  getStamp,
265
- createTimestamp,
266
- upgradeTimestamp,
267
- verifyTimestamp,
268
- listPending
303
+ listStamps,
304
+ logOperation,
305
+ upgradeTimestamp
269
306
  };
@@ -0,0 +1,120 @@
1
+ import {
2
+ getDataDir,
3
+ getStamp,
4
+ insertStamp,
5
+ listStamps,
6
+ logOperation
7
+ } from "./chunk-4S4MSBQL.js";
8
+ import {
9
+ writeAtomic
10
+ } from "./chunk-YFSUDT24.js";
11
+
12
+ // src/tools/create-timestamp.ts
13
+ import { mkdirSync } from "fs";
14
+ import { join } from "path";
15
+ import { randomUUID } from "crypto";
16
+ import { OpenTimestampsClient } from "@otskit/client";
17
+ var HEX64 = /^[0-9a-f]{64}$/i;
18
+ async function createTimestamp(input, db, config) {
19
+ if (!HEX64.test(input.hash)) {
20
+ return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
21
+ }
22
+ const normalizedHash = input.hash.toLowerCase();
23
+ const client = new OpenTimestampsClient({
24
+ calendars: config.calendars,
25
+ resilience: { timeout: config.calendar_timeout_ms }
26
+ });
27
+ const t0 = Date.now();
28
+ let proofBuffer;
29
+ try {
30
+ proofBuffer = await client.stamp(normalizedHash);
31
+ } catch (e) {
32
+ return { error: "calendar_error", details: String(e) };
33
+ }
34
+ const responseTimeMs = Date.now() - t0;
35
+ const id = randomUUID();
36
+ const proofDir = join(getDataDir(), "proofs");
37
+ mkdirSync(proofDir, { recursive: true });
38
+ const proofPath = join(proofDir, `${id}.ots`);
39
+ try {
40
+ writeAtomic(proofPath, proofBuffer);
41
+ } catch (e) {
42
+ return { error: "storage_error", details: String(e) };
43
+ }
44
+ const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
45
+ logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
46
+ return {
47
+ id: record.id,
48
+ hash: record.hash,
49
+ status: "pending",
50
+ calendars: config.calendars,
51
+ created_at: record.created_at,
52
+ proof_path: proofPath
53
+ };
54
+ }
55
+
56
+ // src/tools/verify-timestamp.ts
57
+ import { readFileSync } from "fs";
58
+ import { OpenTimestampsClient as OpenTimestampsClient2 } from "@otskit/client";
59
+ async function verifyTimestamp(input, db, config) {
60
+ const record = getStamp(db, input.id);
61
+ if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
62
+ if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
63
+ let proofBytes;
64
+ try {
65
+ proofBytes = readFileSync(record.proof_path);
66
+ } catch (e) {
67
+ return { error: "storage_error", details: String(e) };
68
+ }
69
+ const client = new OpenTimestampsClient2({
70
+ calendars: config.calendars,
71
+ resilience: { timeout: config.calendar_timeout_ms }
72
+ });
73
+ let result;
74
+ try {
75
+ result = await client.verify(proofBytes, record.hash);
76
+ } catch (e) {
77
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
78
+ return { status: "network_error", hash: record.hash, details: String(e) };
79
+ }
80
+ if (!result.valid) {
81
+ if (result.error?.includes("No Bitcoin attestation")) {
82
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
83
+ return { status: "pending", hash: record.hash, calendars: config.calendars };
84
+ }
85
+ if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
86
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
87
+ return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
88
+ }
89
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
90
+ return { status: "unknown", hash: record.hash };
91
+ }
92
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
93
+ return {
94
+ status: "confirmed",
95
+ hash: record.hash,
96
+ bitcoin_block: result.blockHeight,
97
+ bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
98
+ };
99
+ }
100
+
101
+ // src/tools/list-pending.ts
102
+ function toPublic({ attempt_count: _, last_attempt_at: __, next_retry_at: ___, ...rest }) {
103
+ return rest;
104
+ }
105
+ function listPending(input, db, _config) {
106
+ const result = listStamps(db, {
107
+ status: input.status ?? "pending",
108
+ limit: Math.min(input.limit ?? 50, 200),
109
+ offset: input.offset ?? 0,
110
+ older_than_hours: input.older_than_hours,
111
+ due_now: input.due_now
112
+ });
113
+ return { items: result.items.map(toPublic), total: result.total };
114
+ }
115
+
116
+ export {
117
+ createTimestamp,
118
+ verifyTimestamp,
119
+ listPending
120
+ };
@@ -0,0 +1,79 @@
1
+ import {
2
+ getDb,
3
+ loadConfig,
4
+ upgradeTimestamp
5
+ } from "./chunk-4S4MSBQL.js";
6
+
7
+ // src/tools/watch.ts
8
+ var DEFAULT_WATCH_INTERVAL_MINUTES = 30;
9
+ var MIN_WATCH_INTERVAL_MINUTES = 15;
10
+ var MAX_UPGRADES_PER_TICK = 20;
11
+ function normalizeWatchInterval(intervalMinutes = DEFAULT_WATCH_INTERVAL_MINUTES) {
12
+ if (!Number.isFinite(intervalMinutes)) return DEFAULT_WATCH_INTERVAL_MINUTES;
13
+ return Math.max(MIN_WATCH_INTERVAL_MINUTES, Math.floor(intervalMinutes));
14
+ }
15
+ async function watchPending(intervalMinutes = DEFAULT_WATCH_INTERVAL_MINUTES) {
16
+ const config = loadConfig();
17
+ const db = getDb();
18
+ const minutes = normalizeWatchInterval(intervalMinutes);
19
+ process.stdout.write(`Watching pending stamps and upgrading due proofs every ${minutes} min. Ctrl+C to stop.
20
+
21
+ `);
22
+ async function tick() {
23
+ const dueRows = db.all(
24
+ `SELECT id FROM stamps
25
+ WHERE status = 'pending'
26
+ AND (next_retry_at IS NULL OR next_retry_at <= ?)
27
+ ORDER BY created_at ASC
28
+ LIMIT ?`,
29
+ [(/* @__PURE__ */ new Date()).toISOString(), MAX_UPGRADES_PER_TICK]
30
+ );
31
+ if (dueRows.length > 0) {
32
+ process.stdout.write(`${now()} - upgrading ${dueRows.length} due stamp(s)
33
+ `);
34
+ }
35
+ for (const row of dueRows) {
36
+ const result = await upgradeTimestamp({ id: row.id }, db, config);
37
+ const status = "status" in result ? result.status : `error:${result.error}`;
38
+ process.stdout.write(` upgrade ${row.id.slice(0, 8)} -> ${status}
39
+ `);
40
+ }
41
+ const rows = db.all(
42
+ `SELECT id, hash, status, attempt_count, bitcoin_block, confirmed_at, next_retry_at
43
+ FROM stamps
44
+ WHERE status != 'confirmed'
45
+ ORDER BY created_at DESC`
46
+ );
47
+ const confirmed = db.get(`SELECT COUNT(*) as n FROM stamps WHERE status = 'confirmed'`);
48
+ process.stdout.write(`${now()} - ${rows.length} pending, ${confirmed.n} confirmed
49
+ `);
50
+ for (const row of rows) {
51
+ const next = row.next_retry_at ? ` next ${row.next_retry_at.replace("T", " ").slice(0, 19)}` : "";
52
+ process.stdout.write(` ${row.id.slice(0, 8)} ${row.status} (${row.attempt_count} attempts)${next}
53
+ `);
54
+ }
55
+ if (rows.length === 0) {
56
+ process.stdout.write(` (no pending stamps)
57
+ `);
58
+ }
59
+ process.stdout.write("\n");
60
+ }
61
+ function now() {
62
+ return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
63
+ }
64
+ async function loop() {
65
+ try {
66
+ await tick();
67
+ } catch (e) {
68
+ process.stderr.write(`watch error: ${String(e)}
69
+ `);
70
+ }
71
+ setTimeout(loop, minutes * 60 * 1e3);
72
+ }
73
+ await loop();
74
+ }
75
+
76
+ export {
77
+ normalizeWatchInterval,
78
+ watchPending
79
+ };
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  createTimestamp,
3
3
  listPending,
4
- upgradeTimestamp,
5
4
  verifyTimestamp
6
- } from "./chunk-4WQTV6CC.js";
5
+ } from "./chunk-KVHJHTFL.js";
7
6
  import {
8
7
  backupDb,
9
8
  getDb,
10
- loadConfig
11
- } from "./chunk-Y3F7WBP6.js";
9
+ loadConfig,
10
+ upgradeTimestamp
11
+ } from "./chunk-4S4MSBQL.js";
12
12
  import "./chunk-YFSUDT24.js";
13
13
 
14
14
  // src/cli.ts
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ if (!command || command === "--help" || command === "help") {
7
7
  Commands:
8
8
  serve Start MCP server (stdio transport)
9
9
  setup <target> Configure MCP for an agent (claude | claude-code | codex)
10
- watch [interval] Watch pending stamps in real-time (default: 5 min)
10
+ watch [interval] Watch and upgrade due pending stamps (default: 30 min, minimum: 15 min)
11
11
  stamp <hash> Stamp a SHA-256 hash
12
12
  upgrade <id> Upgrade a pending stamp
13
13
  verify <id> Verify a stamp
@@ -20,7 +20,7 @@ Commands:
20
20
  }
21
21
  switch (command) {
22
22
  case "serve": {
23
- const { runServer } = await import("./server-P3TIJGLB.js");
23
+ const { runServer } = await import("./server-NDG4QZ56.js");
24
24
  await runServer();
25
25
  break;
26
26
  }
@@ -49,11 +49,11 @@ switch (command) {
49
49
  break;
50
50
  }
51
51
  case "watch": {
52
- const { watchPending } = await import("./watch-MF5SD2H6.js");
52
+ const { normalizeWatchInterval, watchPending } = await import("./watch-TQR2L42L.js");
53
53
  const parsed = args[0] ? parseInt(args[0], 10) : NaN;
54
- const interval = isNaN(parsed) || parsed < 1 ? 5 : parsed;
55
- if (args[0] && (isNaN(parsed) || parsed < 1))
56
- process.stderr.write(`Argumento inv\xE1lido "${args[0]}", usando intervalo por defecto: 5 min
54
+ const interval = normalizeWatchInterval(isNaN(parsed) ? void 0 : parsed);
55
+ if (args[0] && (isNaN(parsed) || parsed < 15))
56
+ process.stderr.write(`Invalid interval "${args[0]}", using ${interval} min
57
57
  `);
58
58
  await watchPending(interval);
59
59
  break;
@@ -65,7 +65,7 @@ switch (command) {
65
65
  case "check-pending":
66
66
  case "backup":
67
67
  case "scheduler": {
68
- const { runCli } = await import("./cli-2KSRIDHM.js");
68
+ const { runCli } = await import("./cli-WOUUWARK.js");
69
69
  await runCli(command, args);
70
70
  break;
71
71
  }
@@ -1,14 +1,17 @@
1
1
  import {
2
2
  createTimestamp,
3
- getStamp,
4
3
  listPending,
5
- upgradeTimestamp,
6
4
  verifyTimestamp
7
- } from "./chunk-4WQTV6CC.js";
5
+ } from "./chunk-KVHJHTFL.js";
6
+ import {
7
+ normalizeWatchInterval
8
+ } from "./chunk-XG7NDZR3.js";
8
9
  import {
9
10
  getDb,
10
- loadConfig
11
- } from "./chunk-Y3F7WBP6.js";
11
+ getStamp,
12
+ loadConfig,
13
+ upgradeTimestamp
14
+ } from "./chunk-4S4MSBQL.js";
12
15
  import "./chunk-YFSUDT24.js";
13
16
 
14
17
  // src/server.ts
@@ -61,8 +64,8 @@ function inspectTimestamp(input, db, _config) {
61
64
 
62
65
  // src/tools/watch-window.ts
63
66
  import { exec } from "child_process";
64
- function openWatchWindow(intervalMinutes = 5) {
65
- const minutes = Math.max(1, Math.floor(intervalMinutes));
67
+ function openWatchWindow(intervalMinutes) {
68
+ const minutes = normalizeWatchInterval(intervalMinutes);
66
69
  const cmd = `start powershell.exe -NoExit -Command "ots-mcp watch ${minutes}"`;
67
70
  let errorMsg;
68
71
  exec(cmd, { shell: "cmd" }, (err) => {
@@ -98,7 +101,7 @@ async function hashFileTool(input) {
98
101
  var TOOL_DEFINITIONS = [
99
102
  {
100
103
  name: "create_timestamp",
101
- description: "Stamps a SHA-256 hash against public OpenTimestamps calendars. IMPORTANT: the digest is sent to external calendar servers (alice.btc, bob.btc, finney, catallaxy).",
104
+ description: "Creates a verifiable Bitcoin timestamp for a SHA-256 hash using the OpenTimestamps protocol. Submits the hash to four public OTS calendars (alice.btc, bob.btc, finney, catallaxy) and stores a pending proof locally. Returns a stamp ID to track confirmation status. Confirmation typically takes ~60 minutes but can take several hours during network congestion.",
102
105
  inputSchema: {
103
106
  type: "object",
104
107
  properties: { hash: { type: "string", description: "SHA-256 hex digest (64 chars)" } },
@@ -113,7 +116,7 @@ var TOOL_DEFINITIONS = [
113
116
  },
114
117
  {
115
118
  name: "upgrade_timestamp",
116
- description: "Checks if a pending stamp has been confirmed in Bitcoin. Use the id returned by create_timestamp.",
119
+ description: "Attempts to upgrade a pending OpenTimestamps proof by fetching the latest merkle tree from the calendars. If Bitcoin has included the timestamp, the proof becomes confirmed and the bitcoin_block is recorded. Safe to call repeatedly \u2014 if not yet confirmed, it schedules the next retry automatically.",
117
120
  inputSchema: {
118
121
  type: "object",
119
122
  properties: { id: { type: "string", description: "UUID from the stamp record" } },
@@ -128,7 +131,7 @@ var TOOL_DEFINITIONS = [
128
131
  },
129
132
  {
130
133
  name: "verify_timestamp",
131
- description: "Verifies a stamp against Bitcoin. Does NOT affirm document authorship or truth \u2014 only proves the hash existed before a given Bitcoin block.",
134
+ description: "Verifies a timestamp proof against the Bitcoin blockchain via an Esplora API. Proves that a specific hash existed before a given Bitcoin block height. Does NOT affirm document authorship, content truth, or legal validity \u2014 it only provides a cryptographic proof of existence at a point in time.",
132
135
  inputSchema: {
133
136
  type: "object",
134
137
  properties: { id: { type: "string", description: "UUID from the stamp record" } },
@@ -143,7 +146,7 @@ var TOOL_DEFINITIONS = [
143
146
  },
144
147
  {
145
148
  name: "inspect_timestamp",
146
- description: "Inspects a stored proof file without network calls. Returns calendar_attestations (promises from OTS servers, NOT Bitcoin confirmation) and bitcoin_attestations (actual Bitcoin blocks). A stamp is only truly confirmed when bitcoin_attestations > 0 and bitcoin_confirmed is true.",
149
+ description: "Reads a stored proof file from disk without any network calls. Returns proof metadata including size, number of calendar attestations (pending promises from OTS servers) and Bitcoin attestations (actual confirmed blocks). A stamp is only truly confirmed when bitcoin_attestations > 0 and bitcoin_confirmed is true \u2014 calendar_attestations alone do not prove Bitcoin confirmation.",
147
150
  inputSchema: {
148
151
  type: "object",
149
152
  properties: { id: { type: "string", description: "UUID from the stamp record" } },
@@ -158,7 +161,7 @@ var TOOL_DEFINITIONS = [
158
161
  },
159
162
  {
160
163
  name: "list_pending",
161
- description: "Lists stamp records with status and retry info.",
164
+ description: "Lists stamp records from the local database with their current status, retry count, and next scheduled upgrade time. Filter by status (pending, confirmed, failed), page through results, or find stamps older than N hours. Use this to monitor the state of all timestamped hashes.",
162
165
  inputSchema: {
163
166
  type: "object",
164
167
  properties: {
@@ -177,7 +180,7 @@ var TOOL_DEFINITIONS = [
177
180
  },
178
181
  {
179
182
  name: "hash_file",
180
- description: "Computes the SHA-256 hash of a local file and returns it as a 64-character hex string. No network calls \u2014 pure local operation.",
183
+ description: "Computes the SHA-256 hash of a local file and returns it as a 64-character hex string. Purely local \u2014 no network calls, no data stored. Use this to get the hash before calling create_timestamp, or to verify the integrity of a file independently.",
181
184
  inputSchema: {
182
185
  type: "object",
183
186
  properties: { path: { type: "string", description: "Absolute path to the file" } },
@@ -192,7 +195,7 @@ var TOOL_DEFINITIONS = [
192
195
  },
193
196
  {
194
197
  name: "stamp_file",
195
- description: "Stamps a file against public OpenTimestamps calendars. Computes SHA-256 of the file and stamps it on Bitcoin. IMPORTANT: the digest is sent to external calendar servers (alice.btc, bob.btc, finney, catallaxy).",
198
+ description: "Convenience tool that hashes a local file and stamps it on Bitcoin in one step. Computes the SHA-256 of the file, then submits it to four public OTS calendars. The file contents are never sent externally \u2014 only the hash is. Returns a stamp ID for tracking confirmation.",
196
199
  inputSchema: {
197
200
  type: "object",
198
201
  properties: { path: { type: "string", description: "Absolute path to the file to stamp" } },
@@ -207,18 +210,18 @@ var TOOL_DEFINITIONS = [
207
210
  },
208
211
  {
209
212
  name: "watch",
210
- description: "Opens a new terminal window that monitors pending stamps in real-time, polling at the given interval. The window stays open so the user can watch progress.",
213
+ description: "Opens a new terminal window that continuously monitors pending stamps and attempts due upgrades at each interval. Useful for long-running monitoring sessions after stamping. The window remains open so the user can watch confirmation progress in real time. Minimum interval is 15 minutes to avoid hammering OTS calendars.",
211
214
  inputSchema: {
212
215
  type: "object",
213
216
  properties: {
214
- interval_minutes: { type: "number", description: "Polling interval in minutes (default: 5, minimum: 1)" }
217
+ interval_minutes: { type: "number", description: "Polling interval in minutes (default: 30, minimum: 15)" }
215
218
  }
216
219
  },
217
220
  annotations: {
218
- readOnlyHint: true,
221
+ readOnlyHint: false,
219
222
  destructiveHint: false,
220
223
  idempotentHint: false,
221
- openWorldHint: false
224
+ openWorldHint: true
222
225
  }
223
226
  }
224
227
  ];
@@ -0,0 +1,10 @@
1
+ import {
2
+ normalizeWatchInterval,
3
+ watchPending
4
+ } from "./chunk-XG7NDZR3.js";
5
+ import "./chunk-4S4MSBQL.js";
6
+ import "./chunk-YFSUDT24.js";
7
+ export {
8
+ normalizeWatchInterval,
9
+ watchPending
10
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@otskit/mcp",
3
3
  "mcpName": "io.github.AlexAlves87/otskit-mcp",
4
- "version": "0.6.4",
4
+ "version": "0.7.1",
5
5
  "license": "MIT",
6
6
  "description": "OpenTimestamps MCP server — stamp, upgrade, verify via AI agents",
7
7
  "repository": {
@@ -1,133 +0,0 @@
1
- // src/config.ts
2
- import { readFileSync, existsSync, mkdirSync } from "fs";
3
- import { join } from "path";
4
- import { homedir } from "os";
5
- function getDataDir() {
6
- return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
7
- }
8
- var DEFAULTS = {
9
- stamp_enabled: true,
10
- preserve_enabled: true,
11
- preserve_whitelist: [],
12
- preserve_max_bytes: 104857600,
13
- preserve_max_files: 1e4,
14
- scheduler_interval_minutes: 30,
15
- calendar_timeout_ms: 1e4,
16
- calendar_max_response_bytes: 1048576,
17
- retry_max_attempts: 20,
18
- log_file: join(getDataDir(), "ots-mcp.log"),
19
- calendars: [
20
- "https://alice.btc.calendar.opentimestamps.org",
21
- "https://bob.btc.calendar.opentimestamps.org",
22
- "https://finney.calendar.eternitywall.com",
23
- "https://btc.calendar.catallaxy.com"
24
- ],
25
- esplora_url: "https://blockstream.info/api"
26
- };
27
- function loadConfig() {
28
- const dir = getDataDir();
29
- mkdirSync(dir, { recursive: true });
30
- const configPath = join(dir, "config.json");
31
- if (!existsSync(configPath)) return { ...DEFAULTS };
32
- const raw = JSON.parse(readFileSync(configPath, "utf8"));
33
- return { ...DEFAULTS, ...raw };
34
- }
35
-
36
- // src/db/index.ts
37
- import { createRequire } from "module";
38
- import { join as join2 } from "path";
39
- import { mkdirSync as mkdirSync2, statSync } from "fs";
40
-
41
- // src/db/schema.ts
42
- function initDb(db) {
43
- db.exec("PRAGMA busy_timeout = 5000");
44
- db.exec("PRAGMA foreign_keys = ON");
45
- runMigrations(db);
46
- }
47
- function runMigrations(db) {
48
- const row = db.get("PRAGMA user_version");
49
- if (row.user_version < 1) migrateTo1(db);
50
- }
51
- function migrateTo1(db) {
52
- db.exec("BEGIN");
53
- try {
54
- db.exec(`
55
- CREATE TABLE IF NOT EXISTS stamps (
56
- id TEXT PRIMARY KEY,
57
- hash TEXT NOT NULL,
58
- status TEXT NOT NULL,
59
- created_at TEXT NOT NULL,
60
- confirmed_at TEXT,
61
- bitcoin_block INTEGER,
62
- bitcoin_time TEXT,
63
- proof_path TEXT,
64
- archive_path TEXT,
65
- last_attempt_at TEXT,
66
- attempt_count INTEGER NOT NULL DEFAULT 0,
67
- last_error TEXT,
68
- next_retry_at TEXT,
69
- metadata TEXT
70
- );
71
- CREATE INDEX IF NOT EXISTS idx_stamps_hash ON stamps(hash);
72
- CREATE INDEX IF NOT EXISTS idx_stamps_status ON stamps(status);
73
-
74
- CREATE TABLE IF NOT EXISTS operations_log (
75
- id INTEGER PRIMARY KEY AUTOINCREMENT,
76
- stamp_id TEXT NOT NULL REFERENCES stamps(id),
77
- action TEXT NOT NULL,
78
- result TEXT NOT NULL,
79
- error_msg TEXT,
80
- calendar_uri TEXT,
81
- response_time_ms INTEGER,
82
- created_at TEXT NOT NULL
83
- );
84
- CREATE INDEX IF NOT EXISTS idx_oplog_stamp_id ON operations_log(stamp_id);
85
- CREATE INDEX IF NOT EXISTS idx_oplog_created ON operations_log(created_at);
86
- `);
87
- db.exec("PRAGMA user_version = 1");
88
- db.exec("COMMIT");
89
- } catch (e) {
90
- db.exec("ROLLBACK");
91
- throw e;
92
- }
93
- }
94
-
95
- // src/db/index.ts
96
- var _db = null;
97
- function getDb() {
98
- if (_db) return _db;
99
- const _require = createRequire(import.meta.url);
100
- const { Database } = _require("node-sqlite3-wasm");
101
- const dir = getDataDir();
102
- mkdirSync2(dir, { recursive: true });
103
- _db = new Database(join2(dir, "db.sqlite"));
104
- initDb(_db);
105
- reconcileOrphans(_db);
106
- return _db;
107
- }
108
- function backupDb(destPath) {
109
- const escaped = destPath.replace(/'/g, "''");
110
- getDb().exec(`VACUUM INTO '${escaped}'`);
111
- }
112
- function reconcileOrphans(db) {
113
- const pending = db.all(
114
- `SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
115
- );
116
- for (const row of pending) {
117
- try {
118
- statSync(row.proof_path);
119
- } catch {
120
- db.run(
121
- `UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`,
122
- ["proof file missing on disk", row.id]
123
- );
124
- }
125
- }
126
- }
127
-
128
- export {
129
- getDataDir,
130
- loadConfig,
131
- getDb,
132
- backupDb
133
- };
@@ -1,41 +0,0 @@
1
- import {
2
- getDb,
3
- loadConfig
4
- } from "./chunk-Y3F7WBP6.js";
5
-
6
- // src/tools/watch.ts
7
- async function watchPending(intervalMinutes = 5) {
8
- const config = loadConfig();
9
- const db = getDb();
10
- process.stdout.write(`Watching pending stamps every ${intervalMinutes} min. Ctrl+C to stop.
11
-
12
- `);
13
- function tick() {
14
- const rows = db.all(
15
- `SELECT id, hash, status, attempt_count, bitcoin_block, confirmed_at FROM stamps WHERE status != 'confirmed' ORDER BY created_at DESC`
16
- );
17
- const confirmed = db.get(`SELECT COUNT(*) as n FROM stamps WHERE status = 'confirmed'`);
18
- process.stdout.write(`${now()} \u2014 ${rows.length} pendientes, ${confirmed.n} confirmados
19
- `);
20
- for (const row of rows) {
21
- process.stdout.write(` ${row.id.slice(0, 8)} ${row.status} (${row.attempt_count} intentos)
22
- `);
23
- }
24
- if (rows.length === 0) {
25
- process.stdout.write(` (ning\xFAn sello pendiente)
26
- `);
27
- }
28
- process.stdout.write("\n");
29
- }
30
- function now() {
31
- return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
32
- }
33
- function loop() {
34
- tick();
35
- setTimeout(loop, Math.max(6e4, intervalMinutes * 60 * 1e3));
36
- }
37
- loop();
38
- }
39
- export {
40
- watchPending
41
- };