@otskit/mcp 0.7.1 → 0.7.3
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 +5 -6
- package/dist/{chunk-4S4MSBQL.js → chunk-3FLOLQ5Z.js} +69 -23
- package/dist/{chunk-KVHJHTFL.js → chunk-AEADWLTM.js} +44 -12
- package/dist/chunk-IB2AYNP4.js +70 -0
- package/dist/{chunk-XG7NDZR3.js → chunk-SBGYVIJL.js} +1 -1
- package/dist/{cli-WOUUWARK.js → cli-PHRXQY3H.js} +4 -4
- package/dist/index.js +3 -3
- package/dist/{install-BUSOMBB2.js → install-5QJYMFKV.js} +5 -3
- package/dist/{scheduler-MTE6OUSV.js → scheduler-5IMMUANG.js} +1 -1
- package/dist/{server-NDG4QZ56.js → server-LC7ZJTGO.js} +78 -25
- package/dist/{watch-TQR2L42L.js → watch-EFIJDI2L.js} +3 -3
- package/package.json +4 -2
- package/dist/chunk-YFSUDT24.js +0 -24
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](https://github.com/OTSkit/OTSkit-MCP/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/@otskit/mcp)
|
|
9
9
|
[](https://www.npmjs.com/package/@otskit/mcp)
|
|
10
|
-
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](LICENSE)
|
|
13
13
|
[](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP)
|
|
14
14
|
[](https://smithery.ai/servers/otskit/otskit-mcp)
|
|
15
15
|
|
|
@@ -84,7 +84,6 @@ Create `~/.ots-mcp/config.json` to override defaults:
|
|
|
84
84
|
"scheduler_interval_minutes": 30,
|
|
85
85
|
"retry_max_attempts": 20,
|
|
86
86
|
"calendar_timeout_ms": 10000,
|
|
87
|
-
"esplora_url": "https://blockstream.info/api",
|
|
88
87
|
"calendars": [
|
|
89
88
|
"https://alice.btc.calendar.opentimestamps.org",
|
|
90
89
|
"https://bob.btc.calendar.opentimestamps.org",
|
|
@@ -104,7 +103,7 @@ npm test # run tests
|
|
|
104
103
|
|
|
105
104
|
## Dependencies
|
|
106
105
|
|
|
107
|
-
- [`@otskit/core`](https://github.com/
|
|
108
|
-
- [`@otskit/client`](https://github.com/
|
|
106
|
+
- [`@otskit/core`](https://github.com/OTSkit/otskit-core) - OpenTimestamps core logic
|
|
107
|
+
- [`@otskit/client`](https://github.com/OTSkit/otskit-client) - OTS calendar client
|
|
109
108
|
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) - MCP SDK
|
|
110
109
|
- `node-sqlite3-wasm` - local database (pure WASM, no native compilation)
|
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import {
|
|
2
2
|
writeAtomic
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-IB2AYNP4.js";
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
import { homedir } from "os";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var TRUSTED_CALENDAR_HOSTS = /* @__PURE__ */ new Set([
|
|
11
|
+
"alice.btc.calendar.opentimestamps.org",
|
|
12
|
+
"bob.btc.calendar.opentimestamps.org",
|
|
13
|
+
"finney.calendar.eternitywall.com",
|
|
14
|
+
"btc.calendar.catallaxy.com"
|
|
15
|
+
]);
|
|
16
|
+
var httpsAllowlisted = (hosts) => z.string().refine((v) => {
|
|
17
|
+
try {
|
|
18
|
+
const u = new URL(v);
|
|
19
|
+
return u.protocol === "https:" && hosts.has(u.hostname);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}, { message: "URL must be https and in the host allowlist" });
|
|
24
|
+
var ConfigSchema = z.strictObject({
|
|
25
|
+
stamp_enabled: z.boolean(),
|
|
26
|
+
preserve_enabled: z.boolean(),
|
|
27
|
+
preserve_whitelist: z.array(z.string()),
|
|
28
|
+
preserve_max_bytes: z.number().int().positive().max(10 * 1024 ** 3),
|
|
29
|
+
preserve_max_files: z.number().int().positive().max(1e5),
|
|
30
|
+
scheduler_interval_minutes: z.number().int().min(1).max(1440),
|
|
31
|
+
calendar_timeout_ms: z.number().int().min(1e3).max(6e4),
|
|
32
|
+
retry_max_attempts: z.number().int().min(1).max(100),
|
|
33
|
+
log_file: z.string(),
|
|
34
|
+
calendars: z.array(httpsAllowlisted(TRUSTED_CALENDAR_HOSTS)).min(1).max(10)
|
|
35
|
+
}).partial();
|
|
9
36
|
function getDataDir() {
|
|
10
37
|
return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
|
|
11
38
|
}
|
|
@@ -17,7 +44,6 @@ var DEFAULTS = {
|
|
|
17
44
|
preserve_max_files: 1e4,
|
|
18
45
|
scheduler_interval_minutes: 30,
|
|
19
46
|
calendar_timeout_ms: 1e4,
|
|
20
|
-
calendar_max_response_bytes: 1048576,
|
|
21
47
|
retry_max_attempts: 20,
|
|
22
48
|
log_file: join(getDataDir(), "ots-mcp.log"),
|
|
23
49
|
calendars: [
|
|
@@ -25,16 +51,28 @@ var DEFAULTS = {
|
|
|
25
51
|
"https://bob.btc.calendar.opentimestamps.org",
|
|
26
52
|
"https://finney.calendar.eternitywall.com",
|
|
27
53
|
"https://btc.calendar.catallaxy.com"
|
|
28
|
-
]
|
|
29
|
-
esplora_url: "https://blockstream.info/api"
|
|
54
|
+
]
|
|
30
55
|
};
|
|
31
56
|
function loadConfig() {
|
|
32
57
|
const dir = getDataDir();
|
|
33
58
|
mkdirSync(dir, { recursive: true });
|
|
34
59
|
const configPath = join(dir, "config.json");
|
|
35
60
|
if (!existsSync(configPath)) return { ...DEFAULTS };
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
process.stderr.write(`[ots-mcp] config parse error, using defaults: ${e}
|
|
66
|
+
`);
|
|
67
|
+
return { ...DEFAULTS };
|
|
68
|
+
}
|
|
69
|
+
const parsed = ConfigSchema.safeParse(raw);
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
process.stderr.write(`[ots-mcp] config validation failed, using defaults: ${parsed.error.issues[0]?.message}
|
|
72
|
+
`);
|
|
73
|
+
return { ...DEFAULTS };
|
|
74
|
+
}
|
|
75
|
+
return { ...DEFAULTS, ...parsed.data };
|
|
38
76
|
}
|
|
39
77
|
|
|
40
78
|
// src/db/index.ts
|
|
@@ -122,8 +160,8 @@ function reconcileOrphans(db) {
|
|
|
122
160
|
statSync(row.proof_path);
|
|
123
161
|
} catch {
|
|
124
162
|
db.run(
|
|
125
|
-
`UPDATE stamps SET status = '
|
|
126
|
-
["proof file
|
|
163
|
+
`UPDATE stamps SET status = 'missing_proof', last_error = ? WHERE id = ?`,
|
|
164
|
+
["proof file not found at startup", row.id]
|
|
127
165
|
);
|
|
128
166
|
}
|
|
129
167
|
}
|
|
@@ -241,7 +279,11 @@ async function upgradeTimestamp(input, db, config) {
|
|
|
241
279
|
const proofBefore = readFileSync2(record.proof_path);
|
|
242
280
|
const client = new OpenTimestampsClient({
|
|
243
281
|
calendars: config.calendars,
|
|
244
|
-
resilience: {
|
|
282
|
+
resilience: {
|
|
283
|
+
totalTimeoutMs: config.calendar_timeout_ms,
|
|
284
|
+
connectTimeoutMs: Math.min(config.calendar_timeout_ms, 5e3),
|
|
285
|
+
retries: { enabled: true, maxAttempts: config.retry_max_attempts, backoff: { strategy: "exponential", initialDelayMs: 500, jitter: "full" } }
|
|
286
|
+
}
|
|
245
287
|
});
|
|
246
288
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
247
289
|
const newAttemptCount = record.attempt_count + 1;
|
|
@@ -251,19 +293,22 @@ async function upgradeTimestamp(input, db, config) {
|
|
|
251
293
|
upgraded = await client.upgrade(proofBefore);
|
|
252
294
|
} catch (e) {
|
|
253
295
|
if (e instanceof UpgradeError) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
296
|
+
try {
|
|
297
|
+
const v = await client.verify(proofBefore, record.hash);
|
|
298
|
+
if (v.valid && v.blockHeight != null && v.timestamp != null) {
|
|
299
|
+
const bitcoinTime = new Date(v.timestamp * 1e3).toISOString();
|
|
300
|
+
updateStampStatus(db, input.id, {
|
|
301
|
+
status: "confirmed",
|
|
302
|
+
bitcoin_block: v.blockHeight,
|
|
303
|
+
bitcoin_time: bitcoinTime,
|
|
304
|
+
confirmed_at: now,
|
|
305
|
+
last_attempt_at: now,
|
|
306
|
+
attempt_count: newAttemptCount
|
|
307
|
+
});
|
|
308
|
+
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "success" });
|
|
309
|
+
return { id: input.id, status: "confirmed", bitcoin_block: v.blockHeight, bitcoin_time: bitcoinTime };
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
267
312
|
}
|
|
268
313
|
updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, next_retry_at: next });
|
|
269
314
|
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "pending" });
|
|
@@ -286,7 +331,7 @@ async function upgradeTimestamp(input, db, config) {
|
|
|
286
331
|
attempt_count: newAttemptCount
|
|
287
332
|
});
|
|
288
333
|
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "success" });
|
|
289
|
-
return { id: input.id, status: "confirmed", bitcoin_block: block, bitcoin_time: bitcoinTime
|
|
334
|
+
return { id: input.id, status: "confirmed", bitcoin_block: block, bitcoin_time: bitcoinTime };
|
|
290
335
|
}
|
|
291
336
|
updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, next_retry_at: next });
|
|
292
337
|
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "pending" });
|
|
@@ -300,6 +345,7 @@ export {
|
|
|
300
345
|
backupDb,
|
|
301
346
|
insertStamp,
|
|
302
347
|
getStamp,
|
|
348
|
+
updateStampStatus,
|
|
303
349
|
listStamps,
|
|
304
350
|
logOperation,
|
|
305
351
|
upgradeTimestamp
|
|
@@ -3,14 +3,15 @@ import {
|
|
|
3
3
|
getStamp,
|
|
4
4
|
insertStamp,
|
|
5
5
|
listStamps,
|
|
6
|
-
logOperation
|
|
7
|
-
|
|
6
|
+
logOperation,
|
|
7
|
+
updateStampStatus
|
|
8
|
+
} from "./chunk-3FLOLQ5Z.js";
|
|
8
9
|
import {
|
|
9
10
|
writeAtomic
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-IB2AYNP4.js";
|
|
11
12
|
|
|
12
13
|
// src/tools/create-timestamp.ts
|
|
13
|
-
import { mkdirSync } from "fs";
|
|
14
|
+
import { mkdirSync, unlinkSync } from "fs";
|
|
14
15
|
import { join } from "path";
|
|
15
16
|
import { randomUUID } from "crypto";
|
|
16
17
|
import { OpenTimestampsClient } from "@otskit/client";
|
|
@@ -22,7 +23,11 @@ async function createTimestamp(input, db, config) {
|
|
|
22
23
|
const normalizedHash = input.hash.toLowerCase();
|
|
23
24
|
const client = new OpenTimestampsClient({
|
|
24
25
|
calendars: config.calendars,
|
|
25
|
-
resilience: {
|
|
26
|
+
resilience: {
|
|
27
|
+
totalTimeoutMs: config.calendar_timeout_ms,
|
|
28
|
+
connectTimeoutMs: Math.min(config.calendar_timeout_ms, 5e3),
|
|
29
|
+
retries: { enabled: true, maxAttempts: config.retry_max_attempts, backoff: { strategy: "exponential", initialDelayMs: 500, jitter: "full" } }
|
|
30
|
+
}
|
|
26
31
|
});
|
|
27
32
|
const t0 = Date.now();
|
|
28
33
|
let proofBuffer;
|
|
@@ -41,15 +46,26 @@ async function createTimestamp(input, db, config) {
|
|
|
41
46
|
} catch (e) {
|
|
42
47
|
return { error: "storage_error", details: String(e) };
|
|
43
48
|
}
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
let record;
|
|
50
|
+
db.exec("BEGIN");
|
|
51
|
+
try {
|
|
52
|
+
record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
|
|
53
|
+
logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
|
|
54
|
+
db.exec("COMMIT");
|
|
55
|
+
} catch (e) {
|
|
56
|
+
db.exec("ROLLBACK");
|
|
57
|
+
try {
|
|
58
|
+
unlinkSync(proofPath);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
return { error: "storage_error", details: String(e) };
|
|
62
|
+
}
|
|
46
63
|
return {
|
|
47
64
|
id: record.id,
|
|
48
65
|
hash: record.hash,
|
|
49
66
|
status: "pending",
|
|
50
67
|
calendars: config.calendars,
|
|
51
|
-
created_at: record.created_at
|
|
52
|
-
proof_path: proofPath
|
|
68
|
+
created_at: record.created_at
|
|
53
69
|
};
|
|
54
70
|
}
|
|
55
71
|
|
|
@@ -68,7 +84,11 @@ async function verifyTimestamp(input, db, config) {
|
|
|
68
84
|
}
|
|
69
85
|
const client = new OpenTimestampsClient2({
|
|
70
86
|
calendars: config.calendars,
|
|
71
|
-
resilience: {
|
|
87
|
+
resilience: {
|
|
88
|
+
totalTimeoutMs: config.calendar_timeout_ms,
|
|
89
|
+
connectTimeoutMs: Math.min(config.calendar_timeout_ms, 5e3),
|
|
90
|
+
retries: { enabled: true, maxAttempts: config.retry_max_attempts, backoff: { strategy: "exponential", initialDelayMs: 500, jitter: "full" } }
|
|
91
|
+
}
|
|
72
92
|
});
|
|
73
93
|
let result;
|
|
74
94
|
try {
|
|
@@ -89,17 +109,29 @@ async function verifyTimestamp(input, db, config) {
|
|
|
89
109
|
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
90
110
|
return { status: "unknown", hash: record.hash };
|
|
91
111
|
}
|
|
112
|
+
if (result.blockHeight == null || result.timestamp == null) {
|
|
113
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: "valid:true without blockHeight/timestamp" });
|
|
114
|
+
return { status: "unknown", hash: record.hash };
|
|
115
|
+
}
|
|
116
|
+
const bitcoinTime = new Date(result.timestamp * 1e3).toISOString();
|
|
117
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
118
|
+
updateStampStatus(db, input.id, {
|
|
119
|
+
status: "confirmed",
|
|
120
|
+
bitcoin_block: result.blockHeight,
|
|
121
|
+
bitcoin_time: bitcoinTime,
|
|
122
|
+
confirmed_at: now
|
|
123
|
+
});
|
|
92
124
|
logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
|
|
93
125
|
return {
|
|
94
126
|
status: "confirmed",
|
|
95
127
|
hash: record.hash,
|
|
96
128
|
bitcoin_block: result.blockHeight,
|
|
97
|
-
bitcoin_time:
|
|
129
|
+
bitcoin_time: bitcoinTime
|
|
98
130
|
};
|
|
99
131
|
}
|
|
100
132
|
|
|
101
133
|
// src/tools/list-pending.ts
|
|
102
|
-
function toPublic({ attempt_count:
|
|
134
|
+
function toPublic({ attempt_count: _a, last_attempt_at: _b, next_retry_at: _c, proof_path: _d, archive_path: _e, ...rest }) {
|
|
103
135
|
return rest;
|
|
104
136
|
}
|
|
105
137
|
function listPending(input, db, _config) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { writeFileSync, renameSync, realpathSync, statSync, createReadStream } from "fs";
|
|
4
|
+
import { resolve, sep } from "path";
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
import { pipeline } from "stream/promises";
|
|
7
|
+
import { Transform } from "stream";
|
|
8
|
+
function which(cmd) {
|
|
9
|
+
try {
|
|
10
|
+
const out = execFileSync(
|
|
11
|
+
process.platform === "win32" ? "where" : "which",
|
|
12
|
+
[cmd]
|
|
13
|
+
).toString().trim();
|
|
14
|
+
return out.split("\n")[0] ?? null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeAtomic(dest, data) {
|
|
20
|
+
const tmp = dest + ".tmp";
|
|
21
|
+
writeFileSync(tmp, data);
|
|
22
|
+
renameSync(tmp, dest);
|
|
23
|
+
}
|
|
24
|
+
function escapeXml(raw) {
|
|
25
|
+
return raw.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
26
|
+
}
|
|
27
|
+
function validateFilePath(rawPath, whitelist) {
|
|
28
|
+
let canonical;
|
|
29
|
+
try {
|
|
30
|
+
canonical = realpathSync(rawPath);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { error: "invalid_path", details: String(e?.message ?? e) };
|
|
33
|
+
}
|
|
34
|
+
if (whitelist.length > 0) {
|
|
35
|
+
const allowed = whitelist.some((dir) => {
|
|
36
|
+
const root = resolve(dir);
|
|
37
|
+
return canonical === root || canonical.startsWith(root + sep);
|
|
38
|
+
});
|
|
39
|
+
if (!allowed) return { error: "path_not_allowed", details: `${canonical} is outside allowed directories` };
|
|
40
|
+
}
|
|
41
|
+
let st;
|
|
42
|
+
try {
|
|
43
|
+
st = statSync(canonical);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return { error: "invalid_path", details: String(e?.message ?? e) };
|
|
46
|
+
}
|
|
47
|
+
if (!st.isFile()) return { error: "not_a_regular_file", details: `${canonical} is not a regular file` };
|
|
48
|
+
return { path: canonical };
|
|
49
|
+
}
|
|
50
|
+
async function hashFileStreaming(filePath, maxBytes) {
|
|
51
|
+
const hash = createHash("sha256");
|
|
52
|
+
let bytesRead = 0;
|
|
53
|
+
const limiter = new Transform({
|
|
54
|
+
transform(chunk, _enc, cb) {
|
|
55
|
+
bytesRead += chunk.length;
|
|
56
|
+
if (bytesRead > maxBytes) cb(new Error(`file_too_large: exceeds ${maxBytes} bytes`));
|
|
57
|
+
else cb(null, chunk);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
await pipeline(createReadStream(filePath), limiter, hash);
|
|
61
|
+
return hash.digest("hex");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
which,
|
|
66
|
+
writeAtomic,
|
|
67
|
+
escapeXml,
|
|
68
|
+
validateFilePath,
|
|
69
|
+
hashFileStreaming
|
|
70
|
+
};
|
|
@@ -2,14 +2,14 @@ import {
|
|
|
2
2
|
createTimestamp,
|
|
3
3
|
listPending,
|
|
4
4
|
verifyTimestamp
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-AEADWLTM.js";
|
|
6
6
|
import {
|
|
7
7
|
backupDb,
|
|
8
8
|
getDb,
|
|
9
9
|
loadConfig,
|
|
10
10
|
upgradeTimestamp
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-3FLOLQ5Z.js";
|
|
12
|
+
import "./chunk-IB2AYNP4.js";
|
|
13
13
|
|
|
14
14
|
// src/cli.ts
|
|
15
15
|
async function runCli(command, args) {
|
|
@@ -87,7 +87,7 @@ async function runCli(command, args) {
|
|
|
87
87
|
break;
|
|
88
88
|
}
|
|
89
89
|
case "scheduler": {
|
|
90
|
-
const { runScheduler } = await import("./scheduler-
|
|
90
|
+
const { runScheduler } = await import("./scheduler-5IMMUANG.js");
|
|
91
91
|
await runScheduler(args);
|
|
92
92
|
break;
|
|
93
93
|
}
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ Commands:
|
|
|
20
20
|
}
|
|
21
21
|
switch (command) {
|
|
22
22
|
case "serve": {
|
|
23
|
-
const { runServer } = await import("./server-
|
|
23
|
+
const { runServer } = await import("./server-LC7ZJTGO.js");
|
|
24
24
|
await runServer();
|
|
25
25
|
break;
|
|
26
26
|
}
|
|
@@ -49,7 +49,7 @@ switch (command) {
|
|
|
49
49
|
break;
|
|
50
50
|
}
|
|
51
51
|
case "watch": {
|
|
52
|
-
const { normalizeWatchInterval, watchPending } = await import("./watch-
|
|
52
|
+
const { normalizeWatchInterval, watchPending } = await import("./watch-EFIJDI2L.js");
|
|
53
53
|
const parsed = args[0] ? parseInt(args[0], 10) : NaN;
|
|
54
54
|
const interval = normalizeWatchInterval(isNaN(parsed) ? void 0 : parsed);
|
|
55
55
|
if (args[0] && (isNaN(parsed) || parsed < 15))
|
|
@@ -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-
|
|
68
|
+
const { runCli } = await import("./cli-PHRXQY3H.js");
|
|
69
69
|
await runCli(command, args);
|
|
70
70
|
break;
|
|
71
71
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
+
escapeXml,
|
|
2
3
|
which
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IB2AYNP4.js";
|
|
4
5
|
|
|
5
6
|
// src/scheduler/install.ts
|
|
6
7
|
import { execFileSync } from "child_process";
|
|
7
8
|
import { writeFileSync } from "fs";
|
|
8
9
|
async function installScheduler(args) {
|
|
9
10
|
const intervalIdx = args.indexOf("--interval");
|
|
10
|
-
const
|
|
11
|
+
const parsedInterval = intervalIdx !== -1 ? parseInt(args[intervalIdx + 1] ?? "30") : 30;
|
|
12
|
+
const interval = Math.max(1, Math.min(1440, Number.isFinite(parsedInterval) ? parsedInterval : 30));
|
|
11
13
|
const bin = which("ots-mcp") ?? process.argv[1];
|
|
12
14
|
if (process.platform === "win32") {
|
|
13
15
|
const xmlPath = `${process.env.TEMP}\\ots-mcp-task.xml`;
|
|
@@ -18,7 +20,7 @@ async function installScheduler(args) {
|
|
|
18
20
|
<StartBoundary>2020-01-01T00:00:00</StartBoundary><Enabled>true</Enabled>
|
|
19
21
|
</TimeTrigger></Triggers>
|
|
20
22
|
<Actions><Exec>
|
|
21
|
-
<Command>${bin}</Command>
|
|
23
|
+
<Command>${escapeXml(bin)}</Command>
|
|
22
24
|
<Arguments>check-pending</Arguments>
|
|
23
25
|
</Exec></Actions>
|
|
24
26
|
</Task>`);
|
|
@@ -3,7 +3,7 @@ async function runScheduler(args) {
|
|
|
3
3
|
const [sub, ...rest] = args;
|
|
4
4
|
switch (sub) {
|
|
5
5
|
case "install": {
|
|
6
|
-
const { installScheduler } = await import("./install-
|
|
6
|
+
const { installScheduler } = await import("./install-5QJYMFKV.js");
|
|
7
7
|
await installScheduler(rest);
|
|
8
8
|
break;
|
|
9
9
|
}
|
|
@@ -2,17 +2,20 @@ import {
|
|
|
2
2
|
createTimestamp,
|
|
3
3
|
listPending,
|
|
4
4
|
verifyTimestamp
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-AEADWLTM.js";
|
|
6
6
|
import {
|
|
7
7
|
normalizeWatchInterval
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-SBGYVIJL.js";
|
|
9
9
|
import {
|
|
10
10
|
getDb,
|
|
11
11
|
getStamp,
|
|
12
12
|
loadConfig,
|
|
13
13
|
upgradeTimestamp
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import
|
|
14
|
+
} from "./chunk-3FLOLQ5Z.js";
|
|
15
|
+
import {
|
|
16
|
+
hashFileStreaming,
|
|
17
|
+
validateFilePath
|
|
18
|
+
} from "./chunk-IB2AYNP4.js";
|
|
16
19
|
|
|
17
20
|
// src/server.ts
|
|
18
21
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -53,7 +56,7 @@ function inspectTimestamp(input, db, _config) {
|
|
|
53
56
|
hash: record.hash,
|
|
54
57
|
status: record.status,
|
|
55
58
|
created_at: record.created_at,
|
|
56
|
-
|
|
59
|
+
proof_exists: true,
|
|
57
60
|
proof_size_bytes: proofSize,
|
|
58
61
|
calendar_attestations: calendarAttestations,
|
|
59
62
|
bitcoin_attestations: bitcoinAttestations,
|
|
@@ -75,24 +78,28 @@ function openWatchWindow(intervalMinutes) {
|
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
// src/tools/stamp-file.ts
|
|
78
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
79
|
-
import { OpSHA256 } from "@otskit/core";
|
|
80
81
|
async function stampFile(input, db, config) {
|
|
81
|
-
const
|
|
82
|
-
|
|
82
|
+
const v = validateFilePath(input.path, config.preserve_whitelist);
|
|
83
|
+
if ("error" in v) return v;
|
|
84
|
+
let hash;
|
|
85
|
+
try {
|
|
86
|
+
hash = await hashFileStreaming(v.path, config.preserve_max_bytes);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (String(e?.message).startsWith("file_too_large")) return { error: "file_too_large", details: e.message };
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
83
91
|
return createTimestamp({ hash }, db, config);
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
// src/tools/hash-file.ts
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
async function hashFileTool(input, config) {
|
|
96
|
+
const v = validateFilePath(input.path, config.preserve_whitelist);
|
|
97
|
+
if ("error" in v) return v;
|
|
89
98
|
try {
|
|
90
|
-
const
|
|
91
|
-
return { hash
|
|
99
|
+
const hash = await hashFileStreaming(v.path, config.preserve_max_bytes);
|
|
100
|
+
return { hash };
|
|
92
101
|
} catch (e) {
|
|
93
|
-
if (e?.
|
|
94
|
-
return { error: "file_not_found", details: `File not found: ${input.path}` };
|
|
95
|
-
}
|
|
102
|
+
if (String(e?.message).startsWith("file_too_large")) return { error: "file_too_large", details: e.message };
|
|
96
103
|
throw e;
|
|
97
104
|
}
|
|
98
105
|
}
|
|
@@ -226,6 +233,48 @@ var TOOL_DEFINITIONS = [
|
|
|
226
233
|
}
|
|
227
234
|
];
|
|
228
235
|
|
|
236
|
+
// src/schemas.ts
|
|
237
|
+
import { z } from "zod";
|
|
238
|
+
function parse(schema, args) {
|
|
239
|
+
const r = schema.safeParse(args ?? {});
|
|
240
|
+
if (!r.success) {
|
|
241
|
+
const msg = r.error.issues[0]?.message ?? "invalid input";
|
|
242
|
+
throw new Error(`invalid_params: ${msg}`);
|
|
243
|
+
}
|
|
244
|
+
return r.data;
|
|
245
|
+
}
|
|
246
|
+
var HashInput = z.strictObject({ hash: z.string() });
|
|
247
|
+
var IdInput = z.strictObject({ id: z.string().min(1) });
|
|
248
|
+
var PathInput = z.strictObject({ path: z.string().min(1).max(4096) });
|
|
249
|
+
var ListInput = z.strictObject({
|
|
250
|
+
status: z.enum(["pending", "confirmed", "failed", "timeout", "missing_proof"]).optional(),
|
|
251
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
252
|
+
offset: z.number().int().min(0).optional(),
|
|
253
|
+
older_than_hours: z.number().positive().optional(),
|
|
254
|
+
due_now: z.boolean().optional()
|
|
255
|
+
});
|
|
256
|
+
var WatchInput = z.strictObject({
|
|
257
|
+
interval_minutes: z.number().int().min(15).max(1440).optional()
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// src/feature-gate.ts
|
|
261
|
+
var STAMP_TOOLS = /* @__PURE__ */ new Set([
|
|
262
|
+
"create_timestamp",
|
|
263
|
+
"upgrade_timestamp",
|
|
264
|
+
"verify_timestamp",
|
|
265
|
+
"inspect_timestamp",
|
|
266
|
+
"list_pending",
|
|
267
|
+
"stamp_file",
|
|
268
|
+
"hash_file",
|
|
269
|
+
"watch"
|
|
270
|
+
]);
|
|
271
|
+
var PRESERVE_TOOLS = /* @__PURE__ */ new Set(["stamp_file"]);
|
|
272
|
+
function featureDisabledError(name, config) {
|
|
273
|
+
if (STAMP_TOOLS.has(name) && !config.stamp_enabled) return { error: "feature_disabled", feature: "stamp" };
|
|
274
|
+
if (PRESERVE_TOOLS.has(name) && !config.preserve_enabled) return { error: "feature_disabled", feature: "preserve" };
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
229
278
|
// src/server.ts
|
|
230
279
|
async function runServer() {
|
|
231
280
|
let config = null;
|
|
@@ -244,32 +293,34 @@ async function runServer() {
|
|
|
244
293
|
const { name, arguments: args } = request.params;
|
|
245
294
|
const db = getDb();
|
|
246
295
|
const config2 = getConfig();
|
|
296
|
+
const gate = featureDisabledError(name, config2);
|
|
297
|
+
if (gate) return { content: [{ type: "text", text: JSON.stringify(gate) }], isError: true };
|
|
247
298
|
try {
|
|
248
299
|
let result;
|
|
249
300
|
switch (name) {
|
|
250
301
|
case "create_timestamp":
|
|
251
|
-
result = await createTimestamp(args, db, config2);
|
|
302
|
+
result = await createTimestamp(parse(HashInput, args), db, config2);
|
|
252
303
|
break;
|
|
253
304
|
case "upgrade_timestamp":
|
|
254
|
-
result = await upgradeTimestamp(args, db, config2);
|
|
305
|
+
result = await upgradeTimestamp(parse(IdInput, args), db, config2);
|
|
255
306
|
break;
|
|
256
307
|
case "verify_timestamp":
|
|
257
|
-
result = await verifyTimestamp(args, db, config2);
|
|
308
|
+
result = await verifyTimestamp(parse(IdInput, args), db, config2);
|
|
258
309
|
break;
|
|
259
310
|
case "inspect_timestamp":
|
|
260
|
-
result = inspectTimestamp(args, db, config2);
|
|
311
|
+
result = inspectTimestamp(parse(IdInput, args), db, config2);
|
|
261
312
|
break;
|
|
262
313
|
case "list_pending":
|
|
263
|
-
result = listPending(args, db, config2);
|
|
314
|
+
result = listPending(parse(ListInput, args), db, config2);
|
|
264
315
|
break;
|
|
265
316
|
case "hash_file":
|
|
266
|
-
result = await hashFileTool(args);
|
|
317
|
+
result = await hashFileTool(parse(PathInput, args), config2);
|
|
267
318
|
break;
|
|
268
319
|
case "stamp_file":
|
|
269
|
-
result = await stampFile(args, db, config2);
|
|
320
|
+
result = await stampFile(parse(PathInput, args), db, config2);
|
|
270
321
|
break;
|
|
271
322
|
case "watch":
|
|
272
|
-
result = openWatchWindow(args
|
|
323
|
+
result = openWatchWindow(parse(WatchInput, args).interval_minutes);
|
|
273
324
|
break;
|
|
274
325
|
default:
|
|
275
326
|
return { content: [{ type: "text", text: JSON.stringify({ error: "unknown_tool", tool: name }) }], isError: true };
|
|
@@ -277,7 +328,9 @@ async function runServer() {
|
|
|
277
328
|
const isError = Boolean(result && typeof result === "object" && "error" in result);
|
|
278
329
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError };
|
|
279
330
|
} catch (e) {
|
|
280
|
-
|
|
331
|
+
const details = String(e);
|
|
332
|
+
const code = details.includes("invalid_params") ? "invalid_params" : "internal_error";
|
|
333
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: code, details }) }], isError: true };
|
|
281
334
|
}
|
|
282
335
|
});
|
|
283
336
|
const exit = () => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeWatchInterval,
|
|
3
3
|
watchPending
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-SBGYVIJL.js";
|
|
5
|
+
import "./chunk-3FLOLQ5Z.js";
|
|
6
|
+
import "./chunk-IB2AYNP4.js";
|
|
7
7
|
export {
|
|
8
8
|
normalizeWatchInterval,
|
|
9
9
|
watchPending
|
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.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "OpenTimestamps MCP server — stamp, upgrade, verify via AI agents",
|
|
7
7
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsup src/index.ts --format esm --clean --external node-sqlite3-wasm",
|
|
27
27
|
"dev": "tsup src/index.ts --format esm --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
28
29
|
"test": "vitest run",
|
|
29
30
|
"test:watch": "vitest"
|
|
30
31
|
},
|
|
@@ -32,7 +33,8 @@
|
|
|
32
33
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
34
|
"@otskit/client": "^0.2.0",
|
|
34
35
|
"@otskit/core": "^0.1.0",
|
|
35
|
-
"node-sqlite3-wasm": "0.8.57"
|
|
36
|
+
"node-sqlite3-wasm": "0.8.57",
|
|
37
|
+
"zod": "^4.4.3"
|
|
36
38
|
},
|
|
37
39
|
"pnpm": {
|
|
38
40
|
"onlyBuiltDependencies": [
|
package/dist/chunk-YFSUDT24.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
};
|