@otskit/mcp 0.1.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 +36 -0
- package/dist/chunk-XG7E5YXZ.js +367 -0
- package/dist/chunk-YFSUDT24.js +24 -0
- package/dist/cli-KXB6JLHY.js +96 -0
- package/dist/index.js +41 -0
- package/dist/install-BUSOMBB2.js +37 -0
- package/dist/remove-BGFQRDX3.js +17 -0
- package/dist/scheduler-VVSCSTMC.js +27 -0
- package/dist/server-KWTBEIKF.js +295 -0
- package/dist/status-M6A2RG7G.js +17 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/header.png" alt="OTSkit.ts MCP" width="480" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# @otskit/mcp
|
|
6
|
+
|
|
7
|
+
OpenTimestamps MCP server — stamp, upgrade, and verify timestamps via AI agents (Claude).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm install
|
|
13
|
+
pnpm build
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
ots-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Dependencies
|
|
23
|
+
|
|
24
|
+
- [`@otskit/core`](../otskit-core) — OpenTimestamps core logic
|
|
25
|
+
- [`@otskit/client`](../otskit-client) — OTS client
|
|
26
|
+
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) — MCP SDK
|
|
27
|
+
- `better-sqlite3` — local database
|
|
28
|
+
- `archiver` — ZIP support
|
|
29
|
+
|
|
30
|
+
## Development
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm dev # watch mode
|
|
34
|
+
pnpm test # run tests
|
|
35
|
+
pnpm build # production build
|
|
36
|
+
```
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeAtomic
|
|
3
|
+
} from "./chunk-YFSUDT24.js";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
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 DatabaseConstructor from "better-sqlite3";
|
|
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.pragma("journal_mode = WAL");
|
|
48
|
+
db.pragma("busy_timeout = 5000");
|
|
49
|
+
db.pragma("foreign_keys = ON");
|
|
50
|
+
runMigrations(db);
|
|
51
|
+
}
|
|
52
|
+
function runMigrations(db) {
|
|
53
|
+
const version = db.pragma("user_version", { simple: true });
|
|
54
|
+
if (version < 1) migrateTo1(db);
|
|
55
|
+
}
|
|
56
|
+
function migrateTo1(db) {
|
|
57
|
+
db.transaction(() => {
|
|
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.pragma("user_version = 1");
|
|
92
|
+
})();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/db/index.ts
|
|
96
|
+
var _db = null;
|
|
97
|
+
function getDb() {
|
|
98
|
+
if (_db) return _db;
|
|
99
|
+
const dir = getDataDir();
|
|
100
|
+
mkdirSync2(dir, { recursive: true });
|
|
101
|
+
_db = new DatabaseConstructor(join2(dir, "db.sqlite"));
|
|
102
|
+
initDb(_db);
|
|
103
|
+
reconcileOrphans(_db);
|
|
104
|
+
return _db;
|
|
105
|
+
}
|
|
106
|
+
function backupDb(destPath) {
|
|
107
|
+
getDb().backup(destPath);
|
|
108
|
+
}
|
|
109
|
+
function reconcileOrphans(db) {
|
|
110
|
+
const pending = db.prepare(
|
|
111
|
+
`SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
|
|
112
|
+
).all();
|
|
113
|
+
for (const row of pending) {
|
|
114
|
+
try {
|
|
115
|
+
statSync(row.proof_path);
|
|
116
|
+
} catch {
|
|
117
|
+
db.prepare(`UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`).run("proof file missing on disk", row.id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/tools/create-timestamp.ts
|
|
123
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
124
|
+
import { join as join3 } from "path";
|
|
125
|
+
import { randomUUID } from "crypto";
|
|
126
|
+
import { OpenTimestampsClient } from "@otskit/client";
|
|
127
|
+
|
|
128
|
+
// src/db/stamps.ts
|
|
129
|
+
function insertStamp(db, params) {
|
|
130
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
131
|
+
db.prepare(`
|
|
132
|
+
INSERT INTO stamps (id, hash, status, created_at, proof_path, archive_path, attempt_count, metadata)
|
|
133
|
+
VALUES (?, ?, 'pending', ?, ?, ?, 0, ?)
|
|
134
|
+
`).run(params.id, params.hash, now, params.proof_path, params.archive_path ?? null, params.metadata ?? null);
|
|
135
|
+
return getStamp(db, params.id);
|
|
136
|
+
}
|
|
137
|
+
function getStamp(db, id) {
|
|
138
|
+
return db.prepare("SELECT * FROM stamps WHERE id = ?").get(id) ?? null;
|
|
139
|
+
}
|
|
140
|
+
function updateStampStatus(db, id, params) {
|
|
141
|
+
const fields = [];
|
|
142
|
+
const values = [];
|
|
143
|
+
const add = (col, val) => {
|
|
144
|
+
fields.push(`${col} = ?`);
|
|
145
|
+
values.push(val);
|
|
146
|
+
};
|
|
147
|
+
if (params.status !== void 0) add("status", params.status);
|
|
148
|
+
if (params.bitcoin_block !== void 0) add("bitcoin_block", params.bitcoin_block);
|
|
149
|
+
if (params.bitcoin_time !== void 0) add("bitcoin_time", params.bitcoin_time);
|
|
150
|
+
if (params.confirmed_at !== void 0) add("confirmed_at", params.confirmed_at);
|
|
151
|
+
if (params.last_error !== void 0) add("last_error", params.last_error);
|
|
152
|
+
if (params.attempt_count !== void 0) add("attempt_count", params.attempt_count);
|
|
153
|
+
if (params.last_attempt_at !== void 0) add("last_attempt_at", params.last_attempt_at);
|
|
154
|
+
if (params.next_retry_at !== void 0) add("next_retry_at", params.next_retry_at);
|
|
155
|
+
if (fields.length === 0) return;
|
|
156
|
+
values.push(id);
|
|
157
|
+
db.prepare(`UPDATE stamps SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
158
|
+
}
|
|
159
|
+
function listStamps(db, params) {
|
|
160
|
+
const conds = [];
|
|
161
|
+
const vals = [];
|
|
162
|
+
if (params.status) {
|
|
163
|
+
conds.push("status = ?");
|
|
164
|
+
vals.push(params.status);
|
|
165
|
+
}
|
|
166
|
+
if (params.older_than_hours) {
|
|
167
|
+
const cutoff = new Date(Date.now() - params.older_than_hours * 36e5).toISOString();
|
|
168
|
+
conds.push("created_at < ?");
|
|
169
|
+
vals.push(cutoff);
|
|
170
|
+
}
|
|
171
|
+
const where = conds.length ? `WHERE ${conds.join(" AND ")}` : "";
|
|
172
|
+
const total = db.prepare(`SELECT COUNT(*) as n FROM stamps ${where}`).get(...vals).n;
|
|
173
|
+
const items = db.prepare(
|
|
174
|
+
`SELECT * FROM stamps ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
175
|
+
).all(...vals, params.limit, params.offset);
|
|
176
|
+
return { items, total };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/db/operations-log.ts
|
|
180
|
+
function logOperation(db, params) {
|
|
181
|
+
db.prepare(`
|
|
182
|
+
INSERT INTO operations_log (stamp_id, action, result, error_msg, calendar_uri, response_time_ms, created_at)
|
|
183
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
184
|
+
`).run(
|
|
185
|
+
params.stamp_id,
|
|
186
|
+
params.action,
|
|
187
|
+
params.result,
|
|
188
|
+
params.error_msg ?? null,
|
|
189
|
+
params.calendar_uri ?? null,
|
|
190
|
+
params.response_time_ms ?? null,
|
|
191
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/tools/create-timestamp.ts
|
|
196
|
+
var HEX64 = /^[0-9a-f]{64}$/i;
|
|
197
|
+
async function createTimestamp(input, db, config) {
|
|
198
|
+
if (!HEX64.test(input.hash)) {
|
|
199
|
+
return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
|
|
200
|
+
}
|
|
201
|
+
const normalizedHash = input.hash.toLowerCase();
|
|
202
|
+
const client = new OpenTimestampsClient({
|
|
203
|
+
calendars: config.calendars,
|
|
204
|
+
resilience: { timeout: config.calendar_timeout_ms }
|
|
205
|
+
});
|
|
206
|
+
const t0 = Date.now();
|
|
207
|
+
let proofBuffer;
|
|
208
|
+
try {
|
|
209
|
+
proofBuffer = await client.stamp(normalizedHash);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return { error: "calendar_error", details: String(e) };
|
|
212
|
+
}
|
|
213
|
+
const responseTimeMs = Date.now() - t0;
|
|
214
|
+
const id = randomUUID();
|
|
215
|
+
const proofDir = join3(getDataDir(), "proofs");
|
|
216
|
+
mkdirSync3(proofDir, { recursive: true });
|
|
217
|
+
const proofPath = join3(proofDir, `${id}.ots`);
|
|
218
|
+
try {
|
|
219
|
+
writeAtomic(proofPath, proofBuffer);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return { error: "storage_error", details: String(e) };
|
|
222
|
+
}
|
|
223
|
+
const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
|
|
224
|
+
logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
|
|
225
|
+
return {
|
|
226
|
+
id: record.id,
|
|
227
|
+
hash: record.hash,
|
|
228
|
+
status: "pending",
|
|
229
|
+
calendars: config.calendars,
|
|
230
|
+
created_at: record.created_at,
|
|
231
|
+
proof_path: proofPath
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/tools/upgrade-timestamp.ts
|
|
236
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
237
|
+
import { OpenTimestampsClient as OpenTimestampsClient2, UpgradeError } from "@otskit/client";
|
|
238
|
+
import { deserializeOTS, hasConfirmedAttestation, getEarliestBitcoinBlock } from "@otskit/core";
|
|
239
|
+
function checkBitcoinConfirmation(bytes) {
|
|
240
|
+
try {
|
|
241
|
+
const proof = deserializeOTS(new Uint8Array(bytes));
|
|
242
|
+
const confirmed = hasConfirmedAttestation(proof.attestations);
|
|
243
|
+
if (confirmed) {
|
|
244
|
+
const block = getEarliestBitcoinBlock(proof.attestations);
|
|
245
|
+
return { confirmed: true, block };
|
|
246
|
+
}
|
|
247
|
+
return { confirmed: false };
|
|
248
|
+
} catch {
|
|
249
|
+
return { confirmed: false };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function nextRetryAt(attemptCount) {
|
|
253
|
+
const base = Math.min(3e4 * Math.pow(2, attemptCount), 36e5);
|
|
254
|
+
const jitter = Math.random() * 0.2 * base;
|
|
255
|
+
return new Date(Date.now() + base + jitter).toISOString();
|
|
256
|
+
}
|
|
257
|
+
async function upgradeTimestamp(input, db, config) {
|
|
258
|
+
const record = getStamp(db, input.id);
|
|
259
|
+
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
260
|
+
if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
|
|
261
|
+
const proofBefore = readFileSync2(record.proof_path);
|
|
262
|
+
const client = new OpenTimestampsClient2({
|
|
263
|
+
calendars: config.calendars,
|
|
264
|
+
resilience: { timeout: config.calendar_timeout_ms }
|
|
265
|
+
});
|
|
266
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
267
|
+
const newAttemptCount = record.attempt_count + 1;
|
|
268
|
+
const next = nextRetryAt(newAttemptCount);
|
|
269
|
+
let upgraded;
|
|
270
|
+
try {
|
|
271
|
+
upgraded = await client.upgrade(proofBefore);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
if (e instanceof UpgradeError) {
|
|
274
|
+
updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, next_retry_at: next });
|
|
275
|
+
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "pending" });
|
|
276
|
+
return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
|
|
277
|
+
}
|
|
278
|
+
updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, last_error: String(e), next_retry_at: next });
|
|
279
|
+
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "failed", error_msg: String(e) });
|
|
280
|
+
return { error: "calendar_error", details: String(e) };
|
|
281
|
+
}
|
|
282
|
+
writeAtomic(record.proof_path, upgraded);
|
|
283
|
+
const { confirmed, block } = checkBitcoinConfirmation(upgraded);
|
|
284
|
+
if (confirmed && block !== void 0) {
|
|
285
|
+
const bitcoinTime = now;
|
|
286
|
+
updateStampStatus(db, input.id, {
|
|
287
|
+
status: "confirmed",
|
|
288
|
+
bitcoin_block: block,
|
|
289
|
+
bitcoin_time: bitcoinTime,
|
|
290
|
+
confirmed_at: now,
|
|
291
|
+
last_attempt_at: now,
|
|
292
|
+
attempt_count: newAttemptCount
|
|
293
|
+
});
|
|
294
|
+
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "success" });
|
|
295
|
+
return { id: input.id, status: "confirmed", bitcoin_block: block, bitcoin_time: bitcoinTime, proof_path: record.proof_path };
|
|
296
|
+
}
|
|
297
|
+
updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, next_retry_at: next });
|
|
298
|
+
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "pending" });
|
|
299
|
+
return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/tools/verify-timestamp.ts
|
|
303
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
304
|
+
import { OpenTimestampsClient as OpenTimestampsClient3 } from "@otskit/client";
|
|
305
|
+
async function verifyTimestamp(input, db, config) {
|
|
306
|
+
const record = getStamp(db, input.id);
|
|
307
|
+
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
308
|
+
if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
|
|
309
|
+
let proofBytes;
|
|
310
|
+
try {
|
|
311
|
+
proofBytes = readFileSync3(record.proof_path);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
return { error: "storage_error", details: String(e) };
|
|
314
|
+
}
|
|
315
|
+
const client = new OpenTimestampsClient3({
|
|
316
|
+
calendars: config.calendars,
|
|
317
|
+
resilience: { timeout: config.calendar_timeout_ms }
|
|
318
|
+
});
|
|
319
|
+
let result;
|
|
320
|
+
try {
|
|
321
|
+
result = await client.verify(proofBytes, record.hash);
|
|
322
|
+
} catch (e) {
|
|
323
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
|
|
324
|
+
return { status: "network_error", hash: record.hash, details: String(e) };
|
|
325
|
+
}
|
|
326
|
+
if (!result.valid) {
|
|
327
|
+
if (result.error?.includes("No Bitcoin attestation")) {
|
|
328
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
|
|
329
|
+
return { status: "pending", hash: record.hash, calendars: config.calendars };
|
|
330
|
+
}
|
|
331
|
+
if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
|
|
332
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
333
|
+
return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
|
|
334
|
+
}
|
|
335
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
336
|
+
return { status: "unknown", hash: record.hash };
|
|
337
|
+
}
|
|
338
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
|
|
339
|
+
return {
|
|
340
|
+
status: "confirmed",
|
|
341
|
+
hash: record.hash,
|
|
342
|
+
bitcoin_block: result.blockHeight,
|
|
343
|
+
bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/tools/list-pending.ts
|
|
348
|
+
function listPending(input, db, _config) {
|
|
349
|
+
return listStamps(db, {
|
|
350
|
+
status: input.status ?? "pending",
|
|
351
|
+
limit: Math.min(input.limit ?? 50, 200),
|
|
352
|
+
offset: input.offset ?? 0,
|
|
353
|
+
older_than_hours: input.older_than_hours
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export {
|
|
358
|
+
getDataDir,
|
|
359
|
+
loadConfig,
|
|
360
|
+
getDb,
|
|
361
|
+
backupDb,
|
|
362
|
+
getStamp,
|
|
363
|
+
createTimestamp,
|
|
364
|
+
upgradeTimestamp,
|
|
365
|
+
verifyTimestamp,
|
|
366
|
+
listPending
|
|
367
|
+
};
|
|
@@ -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,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
backupDb,
|
|
3
|
+
createTimestamp,
|
|
4
|
+
getDb,
|
|
5
|
+
listPending,
|
|
6
|
+
loadConfig,
|
|
7
|
+
upgradeTimestamp,
|
|
8
|
+
verifyTimestamp
|
|
9
|
+
} from "./chunk-XG7E5YXZ.js";
|
|
10
|
+
import "./chunk-YFSUDT24.js";
|
|
11
|
+
|
|
12
|
+
// src/cli.ts
|
|
13
|
+
async function runCli(command, args) {
|
|
14
|
+
const config = loadConfig();
|
|
15
|
+
const db = getDb();
|
|
16
|
+
switch (command) {
|
|
17
|
+
case "stamp": {
|
|
18
|
+
const hash = args[0];
|
|
19
|
+
if (!hash) {
|
|
20
|
+
process.stderr.write("Usage: ots-mcp stamp <sha256-hash>\n");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const result = await createTimestamp({ hash }, db, config);
|
|
24
|
+
if ("error" in result) {
|
|
25
|
+
process.stderr.write(`Error: ${result.error} \u2014 ${result.details}
|
|
26
|
+
`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case "upgrade": {
|
|
33
|
+
const id = args[0];
|
|
34
|
+
if (!id) {
|
|
35
|
+
process.stderr.write("Usage: ots-mcp upgrade <id>\n");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const result = await upgradeTimestamp({ id }, db, config);
|
|
39
|
+
if ("error" in result) {
|
|
40
|
+
process.stderr.write(`Error: ${result.error} \u2014 ${result.details}
|
|
41
|
+
`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "verify": {
|
|
48
|
+
const id = args[0];
|
|
49
|
+
if (!id) {
|
|
50
|
+
process.stderr.write("Usage: ots-mcp verify <id>\n");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const result = await verifyTimestamp({ id }, db, config);
|
|
54
|
+
if ("error" in result) {
|
|
55
|
+
process.stderr.write(`Error: ${result.error} \u2014 ${result.details}
|
|
56
|
+
`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "list": {
|
|
63
|
+
const status = args[0] ?? "pending";
|
|
64
|
+
const result = listPending({ status }, db, config);
|
|
65
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "check-pending": {
|
|
69
|
+
const { items } = listPending({ status: "pending", limit: 200 }, db, config);
|
|
70
|
+
process.stderr.write(`Processing ${items.length} pending stamps...
|
|
71
|
+
`);
|
|
72
|
+
for (const record of items) {
|
|
73
|
+
const result = await upgradeTimestamp({ id: record.id }, db, config);
|
|
74
|
+
const statusStr = "status" in result ? result.status : `error:${result.error}`;
|
|
75
|
+
process.stderr.write(`${record.id.slice(0, 8)}: ${statusStr}
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "backup": {
|
|
81
|
+
const dest = args[0] ?? `ots-mcp-backup-${Date.now()}.sqlite`;
|
|
82
|
+
backupDb(dest);
|
|
83
|
+
process.stdout.write(`Backup saved to ${dest}
|
|
84
|
+
`);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "scheduler": {
|
|
88
|
+
const { runScheduler } = await import("./scheduler-VVSCSTMC.js");
|
|
89
|
+
await runScheduler(args);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
runCli
|
|
96
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var [, , command, ...args] = process.argv;
|
|
5
|
+
if (!command || command === "--help" || command === "help") {
|
|
6
|
+
process.stderr.write(`Usage: ots-mcp <command>
|
|
7
|
+
Commands:
|
|
8
|
+
serve Start MCP server (stdio transport)
|
|
9
|
+
stamp <hash> Stamp a SHA-256 hash
|
|
10
|
+
upgrade <id> Upgrade a pending stamp
|
|
11
|
+
verify <id> Verify a stamp
|
|
12
|
+
list [status] List stamps (default: pending)
|
|
13
|
+
check-pending Run pending upgrades (for scheduler)
|
|
14
|
+
backup [dest] Backup the SQLite database
|
|
15
|
+
scheduler Manage OS scheduler (install|remove|status)
|
|
16
|
+
`);
|
|
17
|
+
process.exit(command ? 0 : 1);
|
|
18
|
+
}
|
|
19
|
+
switch (command) {
|
|
20
|
+
case "serve": {
|
|
21
|
+
const { runServer } = await import("./server-KWTBEIKF.js");
|
|
22
|
+
await runServer();
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case "stamp":
|
|
26
|
+
case "upgrade":
|
|
27
|
+
case "verify":
|
|
28
|
+
case "list":
|
|
29
|
+
case "check-pending":
|
|
30
|
+
case "backup":
|
|
31
|
+
case "scheduler": {
|
|
32
|
+
const { runCli } = await import("./cli-KXB6JLHY.js");
|
|
33
|
+
await runCli(command, args);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
default: {
|
|
37
|
+
process.stderr.write(`Unknown command: ${command}. Run 'ots-mcp help' for usage.
|
|
38
|
+
`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
which
|
|
3
|
+
} from "./chunk-YFSUDT24.js";
|
|
4
|
+
|
|
5
|
+
// src/scheduler/install.ts
|
|
6
|
+
import { execFileSync } from "child_process";
|
|
7
|
+
import { writeFileSync } from "fs";
|
|
8
|
+
async function installScheduler(args) {
|
|
9
|
+
const intervalIdx = args.indexOf("--interval");
|
|
10
|
+
const interval = intervalIdx !== -1 ? parseInt(args[intervalIdx + 1] ?? "30") : 30;
|
|
11
|
+
const bin = which("ots-mcp") ?? process.argv[1];
|
|
12
|
+
if (process.platform === "win32") {
|
|
13
|
+
const xmlPath = `${process.env.TEMP}\\ots-mcp-task.xml`;
|
|
14
|
+
writeFileSync(xmlPath, `<?xml version="1.0"?>
|
|
15
|
+
<Task xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
16
|
+
<Triggers><TimeTrigger>
|
|
17
|
+
<Repetition><Interval>PT${interval}M</Interval><StopAtDurationEnd>false</StopAtDurationEnd></Repetition>
|
|
18
|
+
<StartBoundary>2020-01-01T00:00:00</StartBoundary><Enabled>true</Enabled>
|
|
19
|
+
</TimeTrigger></Triggers>
|
|
20
|
+
<Actions><Exec>
|
|
21
|
+
<Command>${bin}</Command>
|
|
22
|
+
<Arguments>check-pending</Arguments>
|
|
23
|
+
</Exec></Actions>
|
|
24
|
+
</Task>`);
|
|
25
|
+
execFileSync("schtasks", ["/create", "/tn", "ots-mcp-check-pending", "/xml", xmlPath, "/f"]);
|
|
26
|
+
process.stdout.write(`Scheduler installed: runs every ${interval} minutes
|
|
27
|
+
`);
|
|
28
|
+
} else {
|
|
29
|
+
process.stdout.write(`Add to crontab (run: crontab -e):
|
|
30
|
+
`);
|
|
31
|
+
process.stdout.write(`*/${interval} * * * * "${bin}" check-pending
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
installScheduler
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/scheduler/remove.ts
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
async function removeScheduler() {
|
|
4
|
+
if (process.platform === "win32") {
|
|
5
|
+
try {
|
|
6
|
+
execFileSync("schtasks", ["/delete", "/tn", "ots-mcp-check-pending", "/f"]);
|
|
7
|
+
process.stdout.write("Scheduler task removed\n");
|
|
8
|
+
} catch {
|
|
9
|
+
process.stderr.write("Task not found or could not be removed\n");
|
|
10
|
+
}
|
|
11
|
+
} else {
|
|
12
|
+
process.stdout.write("Remove the ots-mcp entry from crontab: crontab -e\n");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
removeScheduler
|
|
17
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/scheduler/index.ts
|
|
2
|
+
async function runScheduler(args) {
|
|
3
|
+
const [sub, ...rest] = args;
|
|
4
|
+
switch (sub) {
|
|
5
|
+
case "install": {
|
|
6
|
+
const { installScheduler } = await import("./install-BUSOMBB2.js");
|
|
7
|
+
await installScheduler(rest);
|
|
8
|
+
break;
|
|
9
|
+
}
|
|
10
|
+
case "remove": {
|
|
11
|
+
const { removeScheduler } = await import("./remove-BGFQRDX3.js");
|
|
12
|
+
await removeScheduler();
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
case "status": {
|
|
16
|
+
const { statusScheduler } = await import("./status-M6A2RG7G.js");
|
|
17
|
+
await statusScheduler();
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
default:
|
|
21
|
+
process.stderr.write("Usage: ots-mcp scheduler install [--interval N] | remove | status\n");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
runScheduler
|
|
27
|
+
};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTimestamp,
|
|
3
|
+
getDataDir,
|
|
4
|
+
getDb,
|
|
5
|
+
getStamp,
|
|
6
|
+
listPending,
|
|
7
|
+
loadConfig,
|
|
8
|
+
upgradeTimestamp,
|
|
9
|
+
verifyTimestamp
|
|
10
|
+
} from "./chunk-XG7E5YXZ.js";
|
|
11
|
+
import "./chunk-YFSUDT24.js";
|
|
12
|
+
|
|
13
|
+
// src/server.ts
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
|
|
18
|
+
// src/tools/inspect-timestamp.ts
|
|
19
|
+
import { readFileSync, statSync } from "fs";
|
|
20
|
+
import { deserializeOTS, hasConfirmedAttestation, getEarliestBitcoinBlock } from "@otskit/core";
|
|
21
|
+
function inspectTimestamp(input, db, _config) {
|
|
22
|
+
const record = getStamp(db, input.id);
|
|
23
|
+
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
24
|
+
if (!record.proof_path) return { error: "proof_missing", details: "No proof file on record" };
|
|
25
|
+
let proofBytes;
|
|
26
|
+
let proofSize;
|
|
27
|
+
try {
|
|
28
|
+
proofSize = statSync(record.proof_path).size;
|
|
29
|
+
proofBytes = readFileSync(record.proof_path);
|
|
30
|
+
} catch {
|
|
31
|
+
return { error: "proof_missing", details: `Cannot read proof: ${record.proof_path}` };
|
|
32
|
+
}
|
|
33
|
+
let attestationCount = 0;
|
|
34
|
+
let hasBitcoin = false;
|
|
35
|
+
let bitcoinBlock = null;
|
|
36
|
+
try {
|
|
37
|
+
const proof = deserializeOTS(new Uint8Array(proofBytes));
|
|
38
|
+
attestationCount = proof.attestations.length;
|
|
39
|
+
hasBitcoin = hasConfirmedAttestation(proof.attestations);
|
|
40
|
+
if (hasBitcoin) bitcoinBlock = getEarliestBitcoinBlock(proof.attestations) ?? null;
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
id: record.id,
|
|
45
|
+
hash: record.hash,
|
|
46
|
+
status: record.status,
|
|
47
|
+
created_at: record.created_at,
|
|
48
|
+
proof_path: record.proof_path,
|
|
49
|
+
proof_size_bytes: proofSize,
|
|
50
|
+
attestation_count: attestationCount,
|
|
51
|
+
has_bitcoin_confirmation: hasBitcoin,
|
|
52
|
+
bitcoin_block: bitcoinBlock
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/tools/preserve.ts
|
|
57
|
+
import { createReadStream, createWriteStream, realpathSync, statSync as statSync2, mkdirSync } from "fs";
|
|
58
|
+
import { join, resolve, sep } from "path";
|
|
59
|
+
import { createHash, randomUUID } from "crypto";
|
|
60
|
+
import archiver from "archiver";
|
|
61
|
+
function isPathInWhitelist(resolvedPath, whitelist) {
|
|
62
|
+
const parts = resolvedPath.split(sep);
|
|
63
|
+
return whitelist.some((entry) => {
|
|
64
|
+
const entryParts = resolve(entry).split(sep);
|
|
65
|
+
return entryParts.length <= parts.length && entryParts.every((part, i) => parts[i] === part);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function preserve(input, db, config) {
|
|
69
|
+
if (!config.preserve_enabled || config.preserve_whitelist.length === 0) {
|
|
70
|
+
return { error: "whitelist_not_configured", details: "Set preserve_whitelist in ~/.ots-mcp/config.json" };
|
|
71
|
+
}
|
|
72
|
+
let resolvedInput;
|
|
73
|
+
try {
|
|
74
|
+
resolvedInput = realpathSync(resolve(input.dir_path));
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return { error: "path_not_in_whitelist", details: `Cannot resolve path: ${e}` };
|
|
77
|
+
}
|
|
78
|
+
const resolvedWhitelist = config.preserve_whitelist.map((p) => {
|
|
79
|
+
try {
|
|
80
|
+
return realpathSync(resolve(p));
|
|
81
|
+
} catch {
|
|
82
|
+
return resolve(p);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (!isPathInWhitelist(resolvedInput, resolvedWhitelist)) {
|
|
86
|
+
return { error: "path_not_in_whitelist", details: `${resolvedInput} is not in the configured whitelist` };
|
|
87
|
+
}
|
|
88
|
+
let st;
|
|
89
|
+
try {
|
|
90
|
+
st = statSync2(resolvedInput);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { error: "path_not_in_whitelist", details: String(e) };
|
|
93
|
+
}
|
|
94
|
+
if (!st.isDirectory()) {
|
|
95
|
+
return { error: "path_not_in_whitelist", details: "Path must be a directory" };
|
|
96
|
+
}
|
|
97
|
+
const archiveDir = join(getDataDir(), "archives");
|
|
98
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
99
|
+
const label = input.label ? `-${input.label.replace(/[^a-z0-9-]/gi, "_")}` : "";
|
|
100
|
+
const archivePath = join(archiveDir, `${randomUUID()}${label}.zip`);
|
|
101
|
+
const warnings = [];
|
|
102
|
+
let filesCount = 0;
|
|
103
|
+
await new Promise((res, rej) => {
|
|
104
|
+
const output = createWriteStream(archivePath);
|
|
105
|
+
const arc = archiver("zip", { zlib: { level: 6 } });
|
|
106
|
+
arc.on("warning", (e) => {
|
|
107
|
+
if (e.code === "ENOENT") warnings.push(e.message);
|
|
108
|
+
else rej(e);
|
|
109
|
+
});
|
|
110
|
+
arc.on("error", rej);
|
|
111
|
+
arc.on("entry", () => {
|
|
112
|
+
filesCount++;
|
|
113
|
+
});
|
|
114
|
+
output.on("close", res);
|
|
115
|
+
arc.pipe(output);
|
|
116
|
+
arc.directory(resolvedInput, false);
|
|
117
|
+
arc.finalize();
|
|
118
|
+
});
|
|
119
|
+
const archiveSize = statSync2(archivePath).size;
|
|
120
|
+
if (archiveSize > config.preserve_max_bytes) {
|
|
121
|
+
return { error: "resource_limit_exceeded", details: `Archive ${archiveSize} bytes exceeds limit ${config.preserve_max_bytes}` };
|
|
122
|
+
}
|
|
123
|
+
const hash = await new Promise((res, rej) => {
|
|
124
|
+
const h = createHash("sha256");
|
|
125
|
+
createReadStream(archivePath).on("data", (d) => h.update(d)).on("end", () => res(h.digest("hex"))).on("error", rej);
|
|
126
|
+
});
|
|
127
|
+
const stampResult = await createTimestamp({ hash }, db, config);
|
|
128
|
+
if ("error" in stampResult) return { error: "stamp_error", details: stampResult.details };
|
|
129
|
+
db.prepare("UPDATE stamps SET archive_path = ?, metadata = ? WHERE id = ?").run(
|
|
130
|
+
archivePath,
|
|
131
|
+
JSON.stringify({ source_path: resolvedInput, files_count: filesCount, total_bytes: archiveSize }),
|
|
132
|
+
stampResult.id
|
|
133
|
+
);
|
|
134
|
+
return {
|
|
135
|
+
id: stampResult.id,
|
|
136
|
+
hash,
|
|
137
|
+
archive_path: archivePath,
|
|
138
|
+
proof_path: stampResult.proof_path,
|
|
139
|
+
status: "pending",
|
|
140
|
+
files_count: filesCount,
|
|
141
|
+
archive_size_bytes: archiveSize,
|
|
142
|
+
warnings
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/tool-definitions.ts
|
|
147
|
+
var TOOL_DEFINITIONS = [
|
|
148
|
+
{
|
|
149
|
+
name: "create_timestamp",
|
|
150
|
+
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).",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: { hash: { type: "string", description: "SHA-256 hex digest (64 chars)" } },
|
|
154
|
+
required: ["hash"]
|
|
155
|
+
},
|
|
156
|
+
annotations: {
|
|
157
|
+
readOnlyHint: false,
|
|
158
|
+
destructiveHint: false,
|
|
159
|
+
idempotentHint: false,
|
|
160
|
+
openWorldHint: true
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "upgrade_timestamp",
|
|
165
|
+
description: "Checks if a pending stamp has been confirmed in Bitcoin. Use the id returned by create_timestamp.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: { id: { type: "string", description: "UUID from the stamp record" } },
|
|
169
|
+
required: ["id"]
|
|
170
|
+
},
|
|
171
|
+
annotations: {
|
|
172
|
+
readOnlyHint: false,
|
|
173
|
+
destructiveHint: false,
|
|
174
|
+
idempotentHint: true,
|
|
175
|
+
openWorldHint: true
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "verify_timestamp",
|
|
180
|
+
description: "Verifies a stamp against Bitcoin. Does NOT affirm document authorship or truth \u2014 only proves the hash existed before a given Bitcoin block.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: { id: { type: "string", description: "UUID from the stamp record" } },
|
|
184
|
+
required: ["id"]
|
|
185
|
+
},
|
|
186
|
+
annotations: {
|
|
187
|
+
readOnlyHint: true,
|
|
188
|
+
destructiveHint: false,
|
|
189
|
+
idempotentHint: true,
|
|
190
|
+
openWorldHint: true
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "inspect_timestamp",
|
|
195
|
+
description: "Shows the contents of a stored proof file without making any network calls. Useful for debugging: returns size, parsed attestations, and confirmation status from the proof itself.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: { id: { type: "string", description: "UUID from the stamp record" } },
|
|
199
|
+
required: ["id"]
|
|
200
|
+
},
|
|
201
|
+
annotations: {
|
|
202
|
+
readOnlyHint: true,
|
|
203
|
+
destructiveHint: false,
|
|
204
|
+
idempotentHint: true,
|
|
205
|
+
openWorldHint: false
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "list_pending",
|
|
210
|
+
description: "Lists stamp records with status and retry info.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: {
|
|
214
|
+
status: { type: "string", enum: ["pending", "confirmed", "failed", "timeout"] },
|
|
215
|
+
limit: { type: "number", maximum: 200 },
|
|
216
|
+
offset: { type: "number" },
|
|
217
|
+
older_than_hours: { type: "number" }
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
annotations: {
|
|
221
|
+
readOnlyHint: true,
|
|
222
|
+
destructiveHint: false,
|
|
223
|
+
idempotentHint: true,
|
|
224
|
+
openWorldHint: false
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "preserve",
|
|
229
|
+
description: "FILESYSTEM-SENSITIVE: Compresses a directory to ZIP, stamps its SHA-256 hash, stores archive in whitelist directory. Requires preserve_whitelist config.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: "object",
|
|
232
|
+
properties: {
|
|
233
|
+
dir_path: { type: "string", description: "Absolute path to directory (must be in preserve_whitelist)" },
|
|
234
|
+
label: { type: "string", description: "Optional label for the archive filename" }
|
|
235
|
+
},
|
|
236
|
+
required: ["dir_path"]
|
|
237
|
+
},
|
|
238
|
+
annotations: {
|
|
239
|
+
readOnlyHint: false,
|
|
240
|
+
destructiveHint: false,
|
|
241
|
+
idempotentHint: false,
|
|
242
|
+
openWorldHint: true
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
// src/server.ts
|
|
248
|
+
async function runServer() {
|
|
249
|
+
const config = loadConfig();
|
|
250
|
+
const db = getDb();
|
|
251
|
+
const server = new Server(
|
|
252
|
+
{ name: "ots-mcp", version: "0.1.0" },
|
|
253
|
+
{ capabilities: { tools: {} } }
|
|
254
|
+
);
|
|
255
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
256
|
+
tools: TOOL_DEFINITIONS
|
|
257
|
+
}));
|
|
258
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
259
|
+
const { name, arguments: args } = request.params;
|
|
260
|
+
try {
|
|
261
|
+
let result;
|
|
262
|
+
switch (name) {
|
|
263
|
+
case "create_timestamp":
|
|
264
|
+
result = await createTimestamp(args, db, config);
|
|
265
|
+
break;
|
|
266
|
+
case "upgrade_timestamp":
|
|
267
|
+
result = await upgradeTimestamp(args, db, config);
|
|
268
|
+
break;
|
|
269
|
+
case "verify_timestamp":
|
|
270
|
+
result = await verifyTimestamp(args, db, config);
|
|
271
|
+
break;
|
|
272
|
+
case "inspect_timestamp":
|
|
273
|
+
result = inspectTimestamp(args, db, config);
|
|
274
|
+
break;
|
|
275
|
+
case "list_pending":
|
|
276
|
+
result = listPending(args, db, config);
|
|
277
|
+
break;
|
|
278
|
+
case "preserve":
|
|
279
|
+
result = await preserve(args, db, config);
|
|
280
|
+
break;
|
|
281
|
+
default:
|
|
282
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "unknown_tool", tool: name }) }], isError: true };
|
|
283
|
+
}
|
|
284
|
+
const isError = Boolean(result && typeof result === "object" && "error" in result);
|
|
285
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError };
|
|
286
|
+
} catch (e) {
|
|
287
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "internal_error", details: String(e) }) }], isError: true };
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
const transport = new StdioServerTransport();
|
|
291
|
+
await server.connect(transport);
|
|
292
|
+
}
|
|
293
|
+
export {
|
|
294
|
+
runServer
|
|
295
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/scheduler/status.ts
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
async function statusScheduler() {
|
|
4
|
+
if (process.platform === "win32") {
|
|
5
|
+
try {
|
|
6
|
+
const out = execFileSync("schtasks", ["/query", "/tn", "ots-mcp-check-pending", "/fo", "LIST"]).toString();
|
|
7
|
+
process.stdout.write(out + "\n");
|
|
8
|
+
} catch {
|
|
9
|
+
process.stdout.write("Scheduler task not found\n");
|
|
10
|
+
}
|
|
11
|
+
} else {
|
|
12
|
+
process.stdout.write("Check with: crontab -l | grep ots-mcp\n");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
statusScheduler
|
|
17
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@otskit/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTimestamps MCP server — stamp, upgrade, verify via AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": { "node": ">=18" },
|
|
7
|
+
"bin": { "ots-mcp": "dist/index.js" },
|
|
8
|
+
"exports": "./dist/index.js",
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
12
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@otskit/core": "file:../otskit-core",
|
|
18
|
+
"@otskit/client": "file:../otskit-client",
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
|
+
"better-sqlite3": "^12.10.0",
|
|
21
|
+
"archiver": "^7.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
25
|
+
"@types/archiver": "^6.0.3",
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"tsup": "^8.3.5",
|
|
28
|
+
"typescript": "^5.6.3",
|
|
29
|
+
"vitest": "^2.1.4"
|
|
30
|
+
}
|
|
31
|
+
}
|