@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 CHANGED
@@ -7,9 +7,9 @@
7
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
8
  [![npm version](https://img.shields.io/npm/v/@otskit/mcp.svg)](https://www.npmjs.com/package/@otskit/mcp)
9
9
  [![npm downloads](https://img.shields.io/npm/dt/@otskit/mcp.svg)](https://www.npmjs.com/package/@otskit/mcp)
10
- [![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)
10
+ [![TypeScript](https://img.shields.io/npm/dependency-version/@otskit/mcp/dev/typescript?label=TypeScript)](https://www.typescriptlang.org/)
11
+ [![Node](https://img.shields.io/node/v/@otskit/mcp)](https://nodejs.org)
12
+ [![License](https://img.shields.io/npm/l/@otskit/mcp)](LICENSE)
13
13
  [![Glama](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP/badges/score.svg)](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP)
14
14
  [![smithery badge](https://smithery.ai/badge/otskit/otskit-mcp)](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/AlexAlves87/otskit-core) - OpenTimestamps core logic
108
- - [`@otskit/client`](https://github.com/AlexAlves87/otskit-client) - OTS calendar client
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-YFSUDT24.js";
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
- const raw = JSON.parse(readFileSync(configPath, "utf8"));
37
- return { ...DEFAULTS, ...raw };
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 = 'failed', last_error = ? WHERE id = ?`,
126
- ["proof file missing on disk", row.id]
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: { timeout: config.calendar_timeout_ms }
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
- const { confirmed: confirmed2, block: block2 } = checkBitcoinConfirmation(proofBefore);
255
- if (confirmed2 && block2 !== void 0) {
256
- const bitcoinTime = now;
257
- updateStampStatus(db, input.id, {
258
- status: "confirmed",
259
- bitcoin_block: block2,
260
- bitcoin_time: bitcoinTime,
261
- confirmed_at: now,
262
- last_attempt_at: now,
263
- attempt_count: newAttemptCount
264
- });
265
- logOperation(db, { stamp_id: input.id, action: "upgrade", result: "success" });
266
- return { id: input.id, status: "confirmed", bitcoin_block: block2, bitcoin_time: bitcoinTime, proof_path: record.proof_path };
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, proof_path: record.proof_path };
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
- } from "./chunk-4S4MSBQL.js";
6
+ logOperation,
7
+ updateStampStatus
8
+ } from "./chunk-3FLOLQ5Z.js";
8
9
  import {
9
10
  writeAtomic
10
- } from "./chunk-YFSUDT24.js";
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: { timeout: config.calendar_timeout_ms }
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
- const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
45
- logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
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: { timeout: config.calendar_timeout_ms }
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: new Date(result.timestamp * 1e3).toISOString()
129
+ bitcoin_time: bitcoinTime
98
130
  };
99
131
  }
100
132
 
101
133
  // src/tools/list-pending.ts
102
- function toPublic({ attempt_count: _, last_attempt_at: __, next_retry_at: ___, ...rest }) {
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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,7 +2,7 @@ import {
2
2
  getDb,
3
3
  loadConfig,
4
4
  upgradeTimestamp
5
- } from "./chunk-4S4MSBQL.js";
5
+ } from "./chunk-3FLOLQ5Z.js";
6
6
 
7
7
  // src/tools/watch.ts
8
8
  var DEFAULT_WATCH_INTERVAL_MINUTES = 30;
@@ -2,14 +2,14 @@ import {
2
2
  createTimestamp,
3
3
  listPending,
4
4
  verifyTimestamp
5
- } from "./chunk-KVHJHTFL.js";
5
+ } from "./chunk-AEADWLTM.js";
6
6
  import {
7
7
  backupDb,
8
8
  getDb,
9
9
  loadConfig,
10
10
  upgradeTimestamp
11
- } from "./chunk-4S4MSBQL.js";
12
- import "./chunk-YFSUDT24.js";
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-MTE6OUSV.js");
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-NDG4QZ56.js");
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-TQR2L42L.js");
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-WOUUWARK.js");
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-YFSUDT24.js";
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 interval = intervalIdx !== -1 ? parseInt(args[intervalIdx + 1] ?? "30") : 30;
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-BUSOMBB2.js");
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-KVHJHTFL.js";
5
+ } from "./chunk-AEADWLTM.js";
6
6
  import {
7
7
  normalizeWatchInterval
8
- } from "./chunk-XG7NDZR3.js";
8
+ } from "./chunk-SBGYVIJL.js";
9
9
  import {
10
10
  getDb,
11
11
  getStamp,
12
12
  loadConfig,
13
13
  upgradeTimestamp
14
- } from "./chunk-4S4MSBQL.js";
15
- import "./chunk-YFSUDT24.js";
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
- proof_path: record.proof_path,
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 data = new Uint8Array(readFileSync2(input.path));
82
- const hash = Buffer.from(new OpSHA256().hash(data)).toString("hex");
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
- import { hashFile } from "@otskit/client";
88
- async function hashFileTool(input) {
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 buf = await hashFile(input.path);
91
- return { hash: buf.toString("hex") };
99
+ const hash = await hashFileStreaming(v.path, config.preserve_max_bytes);
100
+ return { hash };
92
101
  } catch (e) {
93
- if (e?.code === "ENOENT") {
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?.interval_minutes);
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
- return { content: [{ type: "text", text: JSON.stringify({ error: "internal_error", details: String(e) }) }], isError: true };
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-XG7NDZR3.js";
5
- import "./chunk-4S4MSBQL.js";
6
- import "./chunk-YFSUDT24.js";
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.1",
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": [
@@ -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
- };