@otskit/mcp 0.7.2 → 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",
@@ -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" });
@@ -5,13 +5,13 @@ import {
5
5
  listStamps,
6
6
  logOperation,
7
7
  updateStampStatus
8
- } from "./chunk-TNLRNQZ3.js";
8
+ } from "./chunk-3FLOLQ5Z.js";
9
9
  import {
10
10
  writeAtomic
11
- } from "./chunk-YFSUDT24.js";
11
+ } from "./chunk-IB2AYNP4.js";
12
12
 
13
13
  // src/tools/create-timestamp.ts
14
- import { mkdirSync } from "fs";
14
+ import { mkdirSync, unlinkSync } from "fs";
15
15
  import { join } from "path";
16
16
  import { randomUUID } from "crypto";
17
17
  import { OpenTimestampsClient } from "@otskit/client";
@@ -23,7 +23,11 @@ async function createTimestamp(input, db, config) {
23
23
  const normalizedHash = input.hash.toLowerCase();
24
24
  const client = new OpenTimestampsClient({
25
25
  calendars: config.calendars,
26
- 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
+ }
27
31
  });
28
32
  const t0 = Date.now();
29
33
  let proofBuffer;
@@ -42,15 +46,26 @@ async function createTimestamp(input, db, config) {
42
46
  } catch (e) {
43
47
  return { error: "storage_error", details: String(e) };
44
48
  }
45
- const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
46
- 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
+ }
47
63
  return {
48
64
  id: record.id,
49
65
  hash: record.hash,
50
66
  status: "pending",
51
67
  calendars: config.calendars,
52
- created_at: record.created_at,
53
- proof_path: proofPath
68
+ created_at: record.created_at
54
69
  };
55
70
  }
56
71
 
@@ -69,7 +84,11 @@ async function verifyTimestamp(input, db, config) {
69
84
  }
70
85
  const client = new OpenTimestampsClient2({
71
86
  calendars: config.calendars,
72
- 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
+ }
73
92
  });
74
93
  let result;
75
94
  try {
@@ -90,6 +109,10 @@ async function verifyTimestamp(input, db, config) {
90
109
  logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
91
110
  return { status: "unknown", hash: record.hash };
92
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
+ }
93
116
  const bitcoinTime = new Date(result.timestamp * 1e3).toISOString();
94
117
  const now = (/* @__PURE__ */ new Date()).toISOString();
95
118
  updateStampStatus(db, input.id, {
@@ -108,7 +131,7 @@ async function verifyTimestamp(input, db, config) {
108
131
  }
109
132
 
110
133
  // src/tools/list-pending.ts
111
- 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 }) {
112
135
  return rest;
113
136
  }
114
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-TNLRNQZ3.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-PWAVQJWD.js";
5
+ } from "./chunk-AEADWLTM.js";
6
6
  import {
7
7
  backupDb,
8
8
  getDb,
9
9
  loadConfig,
10
10
  upgradeTimestamp
11
- } from "./chunk-TNLRNQZ3.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-DBVOG2RS.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-TOXK5UVL.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-XKUZWP2T.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-PWAVQJWD.js";
5
+ } from "./chunk-AEADWLTM.js";
6
6
  import {
7
7
  normalizeWatchInterval
8
- } from "./chunk-2QPOSJTC.js";
8
+ } from "./chunk-SBGYVIJL.js";
9
9
  import {
10
10
  getDb,
11
11
  getStamp,
12
12
  loadConfig,
13
13
  upgradeTimestamp
14
- } from "./chunk-TNLRNQZ3.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-2QPOSJTC.js";
5
- import "./chunk-TNLRNQZ3.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.2",
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
- };