@otskit/mcp 0.6.4 → 0.7.0

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
14
  [![OTSkit-MCP MCP server](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP/badges/card.svg)](https://glama.ai/mcp/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)
@@ -0,0 +1,79 @@
1
+ import {
2
+ getDb,
3
+ loadConfig,
4
+ upgradeTimestamp
5
+ } from "./chunk-ZQUFY5MM.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
+ };
@@ -0,0 +1,120 @@
1
+ import {
2
+ getDataDir,
3
+ getStamp,
4
+ insertStamp,
5
+ listStamps,
6
+ logOperation
7
+ } from "./chunk-ZQUFY5MM.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
+ };
@@ -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, StreamDeserializationContext } 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) {
@@ -159,8 +239,8 @@ async function upgradeTimestamp(input, db, config) {
159
239
  const record = getStamp(db, input.id);
160
240
  if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
161
241
  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({
242
+ const proofBefore = readFileSync2(record.proof_path);
243
+ const client = new OpenTimestampsClient({
164
244
  calendars: config.calendars,
165
245
  resilience: { timeout: config.calendar_timeout_ms }
166
246
  });
@@ -200,70 +280,14 @@ async function upgradeTimestamp(input, db, config) {
200
280
  return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
201
281
  }
202
282
 
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
283
  export {
284
+ getDataDir,
285
+ loadConfig,
286
+ getDb,
287
+ backupDb,
288
+ insertStamp,
264
289
  getStamp,
265
- createTimestamp,
266
- upgradeTimestamp,
267
- verifyTimestamp,
268
- listPending
290
+ listStamps,
291
+ logOperation,
292
+ upgradeTimestamp
269
293
  };
@@ -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-V4ONBNWS.js";
7
6
  import {
8
7
  backupDb,
9
8
  getDb,
10
- loadConfig
11
- } from "./chunk-Y3F7WBP6.js";
9
+ loadConfig,
10
+ upgradeTimestamp
11
+ } from "./chunk-ZQUFY5MM.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-6HE6K337.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-JQ6XJCXF.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-MR24FO2T.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-V4ONBNWS.js";
6
+ import {
7
+ normalizeWatchInterval
8
+ } from "./chunk-KSFL3INC.js";
8
9
  import {
9
10
  getDb,
10
- loadConfig
11
- } from "./chunk-Y3F7WBP6.js";
11
+ getStamp,
12
+ loadConfig,
13
+ upgradeTimestamp
14
+ } from "./chunk-ZQUFY5MM.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) => {
@@ -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 monitors pending stamps and attempts due upgrades at a safe interval. The window stays open so the user can watch progress.",
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-KSFL3INC.js";
5
+ import "./chunk-ZQUFY5MM.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.0",
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
- };