@otskit/mcp 0.1.7 → 0.2.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
@@ -1,98 +1,106 @@
1
- <p align="center">
2
- <img src="https://raw.githubusercontent.com/OTSkit/OTSkit-MCP/master/docs/header.png" alt="OTSkit.ts MCP" width="480" />
3
- </p>
4
-
5
- # @otskit/mcp
6
-
7
- OpenTimestamps MCP server — stamp, upgrade, and verify Bitcoin timestamps via AI agents.
8
-
9
- 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.
10
-
11
- > **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.
12
-
13
- ## Install
14
-
15
- ```bash
16
- npm install -g @otskit/mcp
17
- ```
18
-
19
- ## Agent setup
20
-
21
- ```bash
22
- ots-mcp setup claude # Claude Desktop
23
- ots-mcp setup codex # Codex CLI
24
- ```
25
-
26
- Each command writes the MCP entry into the agent's config file, makes a `.bak` backup if the file already exists, and skips if `ots-mcp` is already configured. Restart the agent afterwards to apply the changes.
27
-
28
- ## CLI commands
29
-
30
- | Command | Description |
31
- |---|---|
32
- | `ots-mcp serve` | Start the MCP server (stdio transport) |
33
- | `ots-mcp stamp <sha256>` | Stamp a SHA-256 hash against Bitcoin calendars |
34
- | `ots-mcp upgrade <id>` | Check if a pending stamp has been confirmed |
35
- | `ots-mcp verify <id>` | Verify a stamp against Bitcoin |
36
- | `ots-mcp list [status]` | List stamps (`pending` / `confirmed` / `failed`) |
37
- | `ots-mcp watch [minutes]` | Poll pending stamps in real-time (default: 5 min) |
38
- | `ots-mcp check-pending` | Run one upgrade pass over all pending stamps |
39
- | `ots-mcp scheduler install\|remove\|status` | Manage OS-level scheduler for auto-upgrades |
40
- | `ots-mcp backup [dest]` | Backup the SQLite database |
41
- | `ots-mcp setup <claude\|codex>` | Configure MCP for an agent |
42
-
43
- ## MCP tools exposed to agents
44
-
45
- | Tool | Description |
46
- |---|---|
47
- | `create_timestamp` | Stamp a SHA-256 hash against 4 public OTS calendars |
48
- | `upgrade_timestamp` | Check if a pending stamp has been confirmed in Bitcoin |
49
- | `verify_timestamp` | Verify a stamp proves hash existed before a given Bitcoin block |
50
- | `inspect_timestamp` | Inspect a stored proof file without network calls |
51
- | `list_pending` | List stamps with status, retry count, and filters |
52
- | `watch` | Open a terminal window monitoring pending stamps in real-time |
53
-
54
- ## Data directory
55
-
56
- All data is stored in `~/.ots-mcp/`:
57
-
58
- ```
59
- ~/.ots-mcp/
60
- ots-mcp.db # SQLite database (stamps, proof files)
61
- config.json # Optional config overrides
62
- ots-mcp.log # Log file
63
- ```
64
-
65
- ## Configuration
66
-
67
- Create `~/.ots-mcp/config.json` to override defaults:
68
-
69
- ```json
70
- {
71
- "stamp_enabled": true,
72
- "scheduler_interval_minutes": 30,
73
- "retry_max_attempts": 20,
74
- "calendar_timeout_ms": 10000,
75
- "esplora_url": "https://blockstream.info/api",
76
- "calendars": [
77
- "https://alice.btc.calendar.opentimestamps.org",
78
- "https://bob.btc.calendar.opentimestamps.org",
79
- "https://finney.calendar.eternitywall.com",
80
- "https://btc.calendar.catallaxy.com"
81
- ]
82
- }
83
- ```
84
-
85
- ## Development
86
-
87
- ```bash
88
- npm run build # production build
89
- npm run dev # watch mode
90
- npm test # run tests
91
- ```
92
-
93
- ## Dependencies
94
-
95
- - [`@otskit/core`](https://github.com/AlexAlves87/otskit-core) — OpenTimestamps core logic
96
- - [`@otskit/client`](https://github.com/AlexAlves87/otskit-client) OTS calendar client
97
- - [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) MCP SDK
98
- - `better-sqlite3` local database
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/OTSkit/OTSkit-MCP/master/docs/header.png" alt="OTSkit.ts MCP" width="480" />
3
+ </p>
4
+
5
+ # @otskit/mcp
6
+
7
+ [![CI](https://github.com/OTSkit/OTSkit-MCP/actions/workflows/ci.yml/badge.svg)](https://github.com/OTSkit/OTSkit-MCP/actions/workflows/ci.yml)
8
+ [![npm version](https://img.shields.io/npm/v/@otskit/mcp.svg)](https://www.npmjs.com/package/@otskit/mcp)
9
+ [![npm downloads](https://img.shields.io/npm/dm/@otskit/mcp.svg)](https://www.npmjs.com/package/@otskit/mcp)
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)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
+
14
+ OpenTimestamps MCP server — stamp, upgrade, and verify Bitcoin timestamps via AI agents.
15
+
16
+ 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.
17
+
18
+ > **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.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install -g @otskit/mcp
24
+ ```
25
+
26
+ ## Agent setup
27
+
28
+ ```bash
29
+ ots-mcp setup claude # Claude Desktop
30
+ ots-mcp setup claude-code # Claude Code CLI
31
+ ots-mcp setup codex # Codex CLI
32
+ ```
33
+
34
+ Each command writes the MCP entry into the agent's config file, makes a `.bak` backup if the file already exists, and skips if `ots-mcp` is already configured. Restart the agent afterwards to apply the changes.
35
+
36
+ ## CLI commands
37
+
38
+ | Command | Description |
39
+ |---|---|
40
+ | `ots-mcp serve` | Start the MCP server (stdio transport) |
41
+ | `ots-mcp stamp <sha256>` | Stamp a SHA-256 hash against Bitcoin calendars |
42
+ | `ots-mcp upgrade <id>` | Check if a pending stamp has been confirmed |
43
+ | `ots-mcp verify <id>` | Verify a stamp against Bitcoin |
44
+ | `ots-mcp list [status]` | List stamps (`pending` / `confirmed` / `failed`) |
45
+ | `ots-mcp watch [minutes]` | Poll pending stamps in real-time (default: 5 min) |
46
+ | `ots-mcp check-pending` | Run one upgrade pass over all pending stamps |
47
+ | `ots-mcp scheduler install\|remove\|status` | Manage OS-level scheduler for auto-upgrades |
48
+ | `ots-mcp backup [dest]` | Backup the SQLite database |
49
+ | `ots-mcp setup <claude\|claude-code\|codex>` | Configure MCP for an agent |
50
+
51
+ ## MCP tools exposed to agents
52
+
53
+ | Tool | Description |
54
+ |---|---|
55
+ | `create_timestamp` | Stamp a SHA-256 hash against 4 public OTS calendars |
56
+ | `upgrade_timestamp` | Check if a pending stamp has been confirmed in Bitcoin |
57
+ | `verify_timestamp` | Verify a stamp — proves hash existed before a given Bitcoin block |
58
+ | `inspect_timestamp` | Inspect a stored proof file without network calls |
59
+ | `list_pending` | List stamps with status, retry count, and filters |
60
+ | `watch` | Open a terminal window monitoring pending stamps in real-time |
61
+
62
+ ## Data directory
63
+
64
+ All data is stored in `~/.ots-mcp/`:
65
+
66
+ ```
67
+ ~/.ots-mcp/
68
+ ots-mcp.db # SQLite database (stamps, proof files)
69
+ config.json # Optional config overrides
70
+ ots-mcp.log # Log file
71
+ ```
72
+
73
+ ## Configuration
74
+
75
+ Create `~/.ots-mcp/config.json` to override defaults:
76
+
77
+ ```json
78
+ {
79
+ "stamp_enabled": true,
80
+ "scheduler_interval_minutes": 30,
81
+ "retry_max_attempts": 20,
82
+ "calendar_timeout_ms": 10000,
83
+ "esplora_url": "https://blockstream.info/api",
84
+ "calendars": [
85
+ "https://alice.btc.calendar.opentimestamps.org",
86
+ "https://bob.btc.calendar.opentimestamps.org",
87
+ "https://finney.calendar.eternitywall.com",
88
+ "https://btc.calendar.catallaxy.com"
89
+ ]
90
+ }
91
+ ```
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ npm run build # production build
97
+ npm run dev # watch mode
98
+ npm test # run tests
99
+ ```
100
+
101
+ ## Dependencies
102
+
103
+ - [`@otskit/core`](https://github.com/AlexAlves87/otskit-core) — OpenTimestamps core logic
104
+ - [`@otskit/client`](https://github.com/AlexAlves87/otskit-client) — OTS calendar client
105
+ - [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) — MCP SDK
106
+ - `better-sqlite3` — local database
@@ -0,0 +1,123 @@
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 DatabaseConstructor from "better-sqlite3";
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.pragma("journal_mode = WAL");
44
+ db.pragma("busy_timeout = 5000");
45
+ db.pragma("foreign_keys = ON");
46
+ runMigrations(db);
47
+ }
48
+ function runMigrations(db) {
49
+ const version = db.pragma("user_version", { simple: true });
50
+ if (version < 1) migrateTo1(db);
51
+ }
52
+ function migrateTo1(db) {
53
+ db.transaction(() => {
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.pragma("user_version = 1");
88
+ })();
89
+ }
90
+
91
+ // src/db/index.ts
92
+ var _db = null;
93
+ function getDb() {
94
+ if (_db) return _db;
95
+ const dir = getDataDir();
96
+ mkdirSync2(dir, { recursive: true });
97
+ _db = new DatabaseConstructor(join2(dir, "db.sqlite"));
98
+ initDb(_db);
99
+ reconcileOrphans(_db);
100
+ return _db;
101
+ }
102
+ function backupDb(destPath) {
103
+ getDb().backup(destPath);
104
+ }
105
+ function reconcileOrphans(db) {
106
+ const pending = db.prepare(
107
+ `SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
108
+ ).all();
109
+ for (const row of pending) {
110
+ try {
111
+ statSync(row.proof_path);
112
+ } catch {
113
+ db.prepare(`UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`).run("proof file missing on disk", row.id);
114
+ }
115
+ }
116
+ }
117
+
118
+ export {
119
+ getDataDir,
120
+ loadConfig,
121
+ getDb,
122
+ backupDb
123
+ };
@@ -0,0 +1,265 @@
1
+ import {
2
+ getDataDir
3
+ } from "./chunk-4ZDPWS7C.js";
4
+ import {
5
+ writeAtomic
6
+ } from "./chunk-YFSUDT24.js";
7
+
8
+ // src/tools/create-timestamp.ts
9
+ import { mkdirSync } from "fs";
10
+ import { join } from "path";
11
+ import { randomUUID } from "crypto";
12
+ import { OpenTimestampsClient } from "@otskit/client";
13
+
14
+ // src/db/stamps.ts
15
+ function insertStamp(db, params) {
16
+ const now = (/* @__PURE__ */ new Date()).toISOString();
17
+ db.prepare(`
18
+ INSERT INTO stamps (id, hash, status, created_at, proof_path, archive_path, attempt_count, metadata)
19
+ VALUES (?, ?, 'pending', ?, ?, ?, 0, ?)
20
+ `).run(params.id, params.hash, now, params.proof_path, params.archive_path ?? null, params.metadata ?? null);
21
+ return getStamp(db, params.id);
22
+ }
23
+ function getStamp(db, id) {
24
+ return db.prepare("SELECT * FROM stamps WHERE id = ?").get(id) ?? null;
25
+ }
26
+ function updateStampStatus(db, id, params) {
27
+ const fields = [];
28
+ const values = [];
29
+ const add = (col, val) => {
30
+ fields.push(`${col} = ?`);
31
+ values.push(val);
32
+ };
33
+ if (params.status !== void 0) add("status", params.status);
34
+ if (params.bitcoin_block !== void 0) add("bitcoin_block", params.bitcoin_block);
35
+ if (params.bitcoin_time !== void 0) add("bitcoin_time", params.bitcoin_time);
36
+ if (params.confirmed_at !== void 0) add("confirmed_at", params.confirmed_at);
37
+ if (params.last_error !== void 0) add("last_error", params.last_error);
38
+ if (params.attempt_count !== void 0) add("attempt_count", params.attempt_count);
39
+ if (params.last_attempt_at !== void 0) add("last_attempt_at", params.last_attempt_at);
40
+ if (params.next_retry_at !== void 0) add("next_retry_at", params.next_retry_at);
41
+ if (fields.length === 0) return;
42
+ values.push(id);
43
+ db.prepare(`UPDATE stamps SET ${fields.join(", ")} WHERE id = ?`).run(...values);
44
+ }
45
+ function listStamps(db, params) {
46
+ const conds = [];
47
+ const vals = [];
48
+ if (params.status) {
49
+ conds.push("status = ?");
50
+ vals.push(params.status);
51
+ }
52
+ if (params.older_than_hours) {
53
+ const cutoff = new Date(Date.now() - params.older_than_hours * 36e5).toISOString();
54
+ conds.push("created_at < ?");
55
+ vals.push(cutoff);
56
+ }
57
+ if (params.due_now) {
58
+ conds.push("(next_retry_at IS NULL OR next_retry_at <= ?)");
59
+ vals.push((/* @__PURE__ */ new Date()).toISOString());
60
+ }
61
+ const where = conds.length ? `WHERE ${conds.join(" AND ")}` : "";
62
+ const total = db.prepare(`SELECT COUNT(*) as n FROM stamps ${where}`).get(...vals).n;
63
+ const items = db.prepare(
64
+ `SELECT * FROM stamps ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
65
+ ).all(...vals, params.limit, params.offset);
66
+ return { items, total };
67
+ }
68
+
69
+ // src/db/operations-log.ts
70
+ function logOperation(db, params) {
71
+ db.prepare(`
72
+ INSERT INTO operations_log (stamp_id, action, result, error_msg, calendar_uri, response_time_ms, created_at)
73
+ VALUES (?, ?, ?, ?, ?, ?, ?)
74
+ `).run(
75
+ params.stamp_id,
76
+ params.action,
77
+ params.result,
78
+ params.error_msg ?? null,
79
+ params.calendar_uri ?? null,
80
+ params.response_time_ms ?? null,
81
+ (/* @__PURE__ */ new Date()).toISOString()
82
+ );
83
+ }
84
+
85
+ // src/tools/create-timestamp.ts
86
+ var HEX64 = /^[0-9a-f]{64}$/i;
87
+ async function createTimestamp(input, db, config) {
88
+ if (!HEX64.test(input.hash)) {
89
+ return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
90
+ }
91
+ const normalizedHash = input.hash.toLowerCase();
92
+ const client = new OpenTimestampsClient({
93
+ calendars: config.calendars,
94
+ resilience: { timeout: config.calendar_timeout_ms }
95
+ });
96
+ const t0 = Date.now();
97
+ let proofBuffer;
98
+ try {
99
+ proofBuffer = await client.stamp(normalizedHash);
100
+ } catch (e) {
101
+ return { error: "calendar_error", details: String(e) };
102
+ }
103
+ const responseTimeMs = Date.now() - t0;
104
+ const id = randomUUID();
105
+ const proofDir = join(getDataDir(), "proofs");
106
+ mkdirSync(proofDir, { recursive: true });
107
+ const proofPath = join(proofDir, `${id}.ots`);
108
+ try {
109
+ writeAtomic(proofPath, proofBuffer);
110
+ } catch (e) {
111
+ return { error: "storage_error", details: String(e) };
112
+ }
113
+ const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
114
+ logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
115
+ return {
116
+ id: record.id,
117
+ hash: record.hash,
118
+ status: "pending",
119
+ calendars: config.calendars,
120
+ created_at: record.created_at,
121
+ proof_path: proofPath
122
+ };
123
+ }
124
+
125
+ // src/tools/upgrade-timestamp.ts
126
+ import { readFileSync } from "fs";
127
+ import { OpenTimestampsClient as OpenTimestampsClient2, UpgradeError } from "@otskit/client";
128
+ import { DetachedTimestampFile, StreamDeserializationContext } from "@otskit/core";
129
+ function collectAttestations(ts) {
130
+ const atts = [...ts.attestations];
131
+ for (const branch of ts.branches) {
132
+ atts.push(...collectAttestations(branch.stamp));
133
+ }
134
+ return atts;
135
+ }
136
+ function checkBitcoinConfirmation(bytes) {
137
+ try {
138
+ const ctx = new StreamDeserializationContext(new Uint8Array(bytes));
139
+ const dtf = DetachedTimestampFile.deserialize(ctx);
140
+ const attestations = collectAttestations(dtf.timestamp);
141
+ const bitcoinAtts = attestations.filter((a) => a.kind === "bitcoin");
142
+ if (bitcoinAtts.length === 0) return { confirmed: false };
143
+ const block = Math.min(...bitcoinAtts.map((a) => a.height));
144
+ return { confirmed: true, block };
145
+ } catch {
146
+ return { confirmed: false };
147
+ }
148
+ }
149
+ function nextRetryAt(attemptCount) {
150
+ const base = Math.min(3e4 * Math.pow(2, attemptCount), 36e5);
151
+ const jitter = Math.random() * 0.2 * base;
152
+ return new Date(Date.now() + base + jitter).toISOString();
153
+ }
154
+ async function upgradeTimestamp(input, db, config) {
155
+ const record = getStamp(db, input.id);
156
+ if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
157
+ if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
158
+ const proofBefore = readFileSync(record.proof_path);
159
+ const client = new OpenTimestampsClient2({
160
+ calendars: config.calendars,
161
+ resilience: { timeout: config.calendar_timeout_ms }
162
+ });
163
+ const now = (/* @__PURE__ */ new Date()).toISOString();
164
+ const newAttemptCount = record.attempt_count + 1;
165
+ const next = nextRetryAt(newAttemptCount);
166
+ let upgraded;
167
+ try {
168
+ upgraded = await client.upgrade(proofBefore);
169
+ } catch (e) {
170
+ if (e instanceof UpgradeError) {
171
+ updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, next_retry_at: next });
172
+ logOperation(db, { stamp_id: input.id, action: "upgrade", result: "pending" });
173
+ return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
174
+ }
175
+ updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, last_error: String(e), next_retry_at: next });
176
+ logOperation(db, { stamp_id: input.id, action: "upgrade", result: "failed", error_msg: String(e) });
177
+ return { error: "calendar_error", details: String(e) };
178
+ }
179
+ writeAtomic(record.proof_path, upgraded);
180
+ const { confirmed, block } = checkBitcoinConfirmation(upgraded);
181
+ if (confirmed && block !== void 0) {
182
+ const bitcoinTime = now;
183
+ updateStampStatus(db, input.id, {
184
+ status: "confirmed",
185
+ bitcoin_block: block,
186
+ bitcoin_time: bitcoinTime,
187
+ confirmed_at: now,
188
+ last_attempt_at: now,
189
+ attempt_count: newAttemptCount
190
+ });
191
+ logOperation(db, { stamp_id: input.id, action: "upgrade", result: "success" });
192
+ return { id: input.id, status: "confirmed", bitcoin_block: block, bitcoin_time: bitcoinTime, proof_path: record.proof_path };
193
+ }
194
+ updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, next_retry_at: next });
195
+ logOperation(db, { stamp_id: input.id, action: "upgrade", result: "pending" });
196
+ return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
197
+ }
198
+
199
+ // src/tools/verify-timestamp.ts
200
+ import { readFileSync as readFileSync2 } from "fs";
201
+ import { OpenTimestampsClient as OpenTimestampsClient3 } from "@otskit/client";
202
+ async function verifyTimestamp(input, db, config) {
203
+ const record = getStamp(db, input.id);
204
+ if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
205
+ if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
206
+ let proofBytes;
207
+ try {
208
+ proofBytes = readFileSync2(record.proof_path);
209
+ } catch (e) {
210
+ return { error: "storage_error", details: String(e) };
211
+ }
212
+ const client = new OpenTimestampsClient3({
213
+ calendars: config.calendars,
214
+ resilience: { timeout: config.calendar_timeout_ms }
215
+ });
216
+ let result;
217
+ try {
218
+ result = await client.verify(proofBytes, record.hash);
219
+ } catch (e) {
220
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
221
+ return { status: "network_error", hash: record.hash, details: String(e) };
222
+ }
223
+ if (!result.valid) {
224
+ if (result.error?.includes("No Bitcoin attestation")) {
225
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
226
+ return { status: "pending", hash: record.hash, calendars: config.calendars };
227
+ }
228
+ if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
229
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
230
+ return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
231
+ }
232
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
233
+ return { status: "unknown", hash: record.hash };
234
+ }
235
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
236
+ return {
237
+ status: "confirmed",
238
+ hash: record.hash,
239
+ bitcoin_block: result.blockHeight,
240
+ bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
241
+ };
242
+ }
243
+
244
+ // src/tools/list-pending.ts
245
+ function toPublic({ attempt_count: _, last_attempt_at: __, next_retry_at: ___, ...rest }) {
246
+ return rest;
247
+ }
248
+ function listPending(input, db, _config) {
249
+ const result = listStamps(db, {
250
+ status: input.status ?? "pending",
251
+ limit: Math.min(input.limit ?? 50, 200),
252
+ offset: input.offset ?? 0,
253
+ older_than_hours: input.older_than_hours,
254
+ due_now: input.due_now
255
+ });
256
+ return { items: result.items.map(toPublic), total: result.total };
257
+ }
258
+
259
+ export {
260
+ getStamp,
261
+ createTimestamp,
262
+ upgradeTimestamp,
263
+ verifyTimestamp,
264
+ listPending
265
+ };
@@ -0,0 +1,24 @@
1
+ // src/utils.ts
2
+ import { execFileSync } from "child_process";
3
+ import { writeFileSync, renameSync } from "fs";
4
+ function which(cmd) {
5
+ try {
6
+ const out = execFileSync(
7
+ process.platform === "win32" ? "where" : "which",
8
+ [cmd]
9
+ ).toString().trim();
10
+ return out.split("\n")[0] ?? null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function writeAtomic(dest, data) {
16
+ const tmp = dest + ".tmp";
17
+ writeFileSync(tmp, data);
18
+ renameSync(tmp, dest);
19
+ }
20
+
21
+ export {
22
+ which,
23
+ writeAtomic
24
+ };
@@ -0,0 +1,54 @@
1
+ // src/setup/claude.ts
2
+ import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ function getConfigPath() {
6
+ if (process.platform === "win32") {
7
+ return join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
8
+ } else if (process.platform === "darwin") {
9
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
10
+ } else {
11
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
12
+ }
13
+ }
14
+ function setupClaude() {
15
+ const configPath = getConfigPath();
16
+ const configDir = join(configPath, "..");
17
+ if (!existsSync(configDir)) {
18
+ mkdirSync(configDir, { recursive: true });
19
+ }
20
+ let config = {};
21
+ if (existsSync(configPath)) {
22
+ try {
23
+ config = JSON.parse(readFileSync(configPath, "utf8"));
24
+ } catch {
25
+ process.stderr.write(` Aviso: no se pudo parsear la config existente, se crear\xE1 nueva.
26
+ `);
27
+ }
28
+ const existing = config.mcpServers ?? {};
29
+ if ("otskit" in existing) {
30
+ process.stdout.write(` ots-mcp ya est\xE1 configurado en ${configPath}
31
+ `);
32
+ process.stdout.write(` No se hizo ning\xFAn cambio.
33
+ `);
34
+ return;
35
+ }
36
+ const backupPath = configPath + ".bak";
37
+ copyFileSync(configPath, backupPath);
38
+ process.stdout.write(` Backup guardado en ${backupPath}
39
+ `);
40
+ }
41
+ const mcpServers = config.mcpServers ?? {};
42
+ mcpServers["otskit"] = { command: "npx", args: ["-y", "@otskit/mcp", "serve"] };
43
+ config.mcpServers = mcpServers;
44
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
45
+ process.stdout.write(`OTSkit MCP configurado para Claude Desktop
46
+ `);
47
+ process.stdout.write(` Config: ${configPath}
48
+ `);
49
+ process.stdout.write(` Reinicia Claude Desktop para aplicar los cambios.
50
+ `);
51
+ }
52
+ export {
53
+ setupClaude
54
+ };