@mikeargento/bitgraph-mcp 0.1.2 → 0.3.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 +7 -5
- package/dist/__tests__/format.test.js +4 -4
- package/dist/__tests__/format.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +45 -58
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/format.d.ts +11 -4
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +17 -12
- package/dist/format.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/server.d.ts +22 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +114 -87
- package/dist/server.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/format.test.ts +4 -4
- package/src/__tests__/tools.test.ts +50 -67
- package/src/format.ts +29 -17
- package/src/server.ts +131 -109
package/src/format.ts
CHANGED
|
@@ -31,18 +31,25 @@ export function proofUrl(
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* One
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* One outcome per path, in the product's own vocabulary. "fused": a new fused
|
|
35
|
+
* artifact was built from the file and committed under its own slot. "on
|
|
36
|
+
* record": the bytes already had a recording or a fused artifact naming them
|
|
37
|
+
* as origin, and nothing was minted. "not fused": the attempt failed; never
|
|
38
|
+
* claim "on record" for bytes that have no proof.
|
|
37
39
|
*/
|
|
38
40
|
export interface RecordOutcome {
|
|
39
41
|
path: string;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
/** The file's own digest (URL-safe): the origin of the fused artifact. */
|
|
43
|
+
digest: string;
|
|
44
|
+
outcome: "fused" | "on record" | "not fused";
|
|
45
|
+
/** The fused artifact's digest (URL-safe), present on a "fused" outcome. */
|
|
46
|
+
artifact_digest: string | null;
|
|
47
|
+
placement: string | null;
|
|
42
48
|
counter: string | null;
|
|
43
49
|
epoch: string | null; // URL-safe
|
|
44
50
|
total_positions: number;
|
|
45
51
|
proof_url: string | null;
|
|
52
|
+
error?: string;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
export interface CheckOutcome {
|
|
@@ -60,33 +67,38 @@ export function positionOf(proof: BitGraphProof): { counter: string | null; epoc
|
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
export function renderRecordMarkdown(outcomes: readonly RecordOutcome[]): string {
|
|
63
|
-
const
|
|
70
|
+
const fused = outcomes.filter((o) => o.outcome === "fused");
|
|
64
71
|
const onRecord = outcomes.filter((o) => o.outcome === "on record");
|
|
65
|
-
const
|
|
72
|
+
const notFused = outcomes.filter((o) => o.outcome === "not fused");
|
|
66
73
|
const lines: string[] = [];
|
|
67
|
-
let headline = `${
|
|
68
|
-
if (
|
|
69
|
-
headline = `${
|
|
74
|
+
let headline = `${fused.length} fused, ${onRecord.length} already on record.`;
|
|
75
|
+
if (notFused.length > 0) {
|
|
76
|
+
headline = `${fused.length} fused, ${onRecord.length} already on record, ${notFused.length} NOT fused.`;
|
|
70
77
|
}
|
|
71
78
|
lines.push(headline);
|
|
72
79
|
for (const o of outcomes) {
|
|
73
|
-
if (o.outcome === "not
|
|
74
|
-
lines.push(`- not
|
|
80
|
+
if (o.outcome === "not fused") {
|
|
81
|
+
lines.push(`- not fused · ${o.path}${o.error ? `: ${o.error}` : ""}`);
|
|
75
82
|
continue;
|
|
76
83
|
}
|
|
77
|
-
const
|
|
84
|
+
const note =
|
|
78
85
|
o.outcome === "on record"
|
|
79
86
|
? o.total_positions > 1
|
|
80
|
-
? ` (${o.total_positions}
|
|
87
|
+
? ` (${o.total_positions} positions, earliest shown)`
|
|
81
88
|
: ""
|
|
82
|
-
:
|
|
89
|
+
: o.placement
|
|
90
|
+
? ` (${o.placement})`
|
|
91
|
+
: "";
|
|
92
|
+
lines.push(`- ${o.outcome} · #${o.counter ?? "?"} · ${o.path}${note}\n ${o.proof_url}`);
|
|
93
|
+
}
|
|
94
|
+
if (fused.length > 0) {
|
|
83
95
|
lines.push(
|
|
84
|
-
|
|
96
|
+
"\nEach fused artifact was built in memory from the file, hashed and committed under its own slot; the file itself is unchanged and was not uploaded. The original plus the proof rebuilds the fused bytes; the Frame for each is in the structured result."
|
|
85
97
|
);
|
|
86
98
|
}
|
|
87
99
|
if (onRecord.length > 0) {
|
|
88
100
|
lines.push(
|
|
89
|
-
|
|
101
|
+
"\nFiles already on record were left alone. To make a new fused artifact from one of them deliberately, call bitgraph_record with again=true."
|
|
90
102
|
);
|
|
91
103
|
}
|
|
92
104
|
return lines.join("\n");
|
package/src/server.ts
CHANGED
|
@@ -3,21 +3,24 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @mikeargento/bitgraph-mcp: tool definitions.
|
|
5
5
|
*
|
|
6
|
-
* Three gestures, the same three the website has:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Three gestures, the same three the website has: make a BitGraph of a file,
|
|
7
|
+
* check whether bytes are on record, fetch a proof. Making a BitGraph builds a
|
|
8
|
+
* fused artifact from the file in memory, on this machine, and commits its
|
|
9
|
+
* digest under a slot allocated for it; only SHA-256 digests and slot records
|
|
10
|
+
* ever leave the machine. File contents are never uploaded.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
14
|
import { z } from "zod";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { fuse, builderFor, placementForBytes, fusedNamesFor } from "@mikeargento/bitgraph";
|
|
13
17
|
import {
|
|
14
18
|
ApiError,
|
|
15
|
-
PartialCommitError,
|
|
16
19
|
batchCheck,
|
|
17
|
-
commitDigests,
|
|
18
20
|
configFromEnv,
|
|
19
21
|
getProofDetail,
|
|
20
22
|
search,
|
|
23
|
+
type ApiConfig,
|
|
21
24
|
} from "./api.js";
|
|
22
25
|
import {
|
|
23
26
|
fromUrlSafeB64,
|
|
@@ -38,10 +41,74 @@ import {
|
|
|
38
41
|
} from "./format.js";
|
|
39
42
|
import type { BitGraphProof } from "./types.js";
|
|
40
43
|
|
|
41
|
-
export const SERVER_VERSION = "0.
|
|
44
|
+
export const SERVER_VERSION = "0.2.0";
|
|
42
45
|
|
|
43
46
|
const HASH_CONCURRENCY = 4;
|
|
44
47
|
const MAX_FILES = 500;
|
|
48
|
+
/** Files above this are refused: the fused artifact is built in memory. */
|
|
49
|
+
const MAX_FUSE_BYTES = 256 * 1024 * 1024;
|
|
50
|
+
|
|
51
|
+
/** What making a BitGraph of one file yields. */
|
|
52
|
+
export interface FusedSummary {
|
|
53
|
+
proof: BitGraphProof;
|
|
54
|
+
frame: unknown;
|
|
55
|
+
placement: string;
|
|
56
|
+
artifactDigestB64: string;
|
|
57
|
+
originDigestB64: string;
|
|
58
|
+
}
|
|
59
|
+
export type FuseFileFn = (bytes: Uint8Array, name: string, config: ApiConfig) => Promise<FusedSummary>;
|
|
60
|
+
export interface ServerDeps {
|
|
61
|
+
/** The fuse pipeline; tests inject a stand-in. Default: the core package's fuse() against the configured site. */
|
|
62
|
+
fuseFile?: FuseFileFn;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The default pipeline, the same one the site's drop and the bitgraph-fuse
|
|
67
|
+
* command run: choose the placement from the bytes, allocate a slot, derive
|
|
68
|
+
* the commitment, build the fused bytes, hash them, commit under that exact
|
|
69
|
+
* slot, verify the returned proof against the bytes.
|
|
70
|
+
*/
|
|
71
|
+
async function fuseFileDefault(bytes: Uint8Array, name: string, config: ApiConfig): Promise<FusedSummary> {
|
|
72
|
+
const placement = placementForBytes(bytes);
|
|
73
|
+
const { fusedName } = fusedNamesFor(name, placement);
|
|
74
|
+
const r = await fuse(builderFor(placement, bytes), {
|
|
75
|
+
placement,
|
|
76
|
+
original: bytes,
|
|
77
|
+
fusedFile: fusedName,
|
|
78
|
+
keepFused: false,
|
|
79
|
+
transport: { baseUrl: config.baseUrl, ...(config.apiKey ? { apiKey: config.apiKey } : {}) },
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
proof: r.proof as unknown as BitGraphProof,
|
|
83
|
+
frame: r.frame,
|
|
84
|
+
placement,
|
|
85
|
+
artifactDigestB64: r.artifactDigestB64,
|
|
86
|
+
originDigestB64: r.originDigestB64 ?? "",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Read the given paths whole (bounded concurrency). Throws before any network call. */
|
|
91
|
+
async function readPaths(paths: readonly string[]): Promise<Array<{ bytes: Uint8Array; digestB64: string }>> {
|
|
92
|
+
const failures: string[] = [];
|
|
93
|
+
const out = await mapConcurrent(paths, HASH_CONCURRENCY, async (p) => {
|
|
94
|
+
try {
|
|
95
|
+
const buf = await readFile(p);
|
|
96
|
+
if (buf.length > MAX_FUSE_BYTES) throw new Error(`larger than ${MAX_FUSE_BYTES / (1024 * 1024)} MB; the fused artifact is built in memory`);
|
|
97
|
+
const bytes = new Uint8Array(buf);
|
|
98
|
+
const digestB64 = await sha256FileB64(p);
|
|
99
|
+
return { bytes, digestB64 };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
failures.push(`${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
102
|
+
return { bytes: new Uint8Array(0), digestB64: "" };
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
if (failures.length > 0) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Could not read ${failures.length} file(s); nothing was BitGraphed.\n${failures.join("\n")}\nUse absolute paths to existing regular files under ${MAX_FUSE_BYTES / (1024 * 1024)} MB.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
45
112
|
|
|
46
113
|
const responseFormatSchema = z
|
|
47
114
|
.enum(["markdown", "json"])
|
|
@@ -91,7 +158,8 @@ async function hashPaths(paths: readonly string[]): Promise<string[]> {
|
|
|
91
158
|
return digests;
|
|
92
159
|
}
|
|
93
160
|
|
|
94
|
-
export function buildServer(): McpServer {
|
|
161
|
+
export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
162
|
+
const fuseFile = deps.fuseFile ?? fuseFileDefault;
|
|
95
163
|
const server = new McpServer({
|
|
96
164
|
name: "bitgraph-mcp-server",
|
|
97
165
|
version: SERVER_VERSION,
|
|
@@ -100,36 +168,26 @@ export function buildServer(): McpServer {
|
|
|
100
168
|
server.registerTool(
|
|
101
169
|
"bitgraph_record",
|
|
102
170
|
{
|
|
103
|
-
title: "
|
|
171
|
+
title: "Make a BitGraph",
|
|
104
172
|
description:
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"Files whose bytes are already on record are NOT
|
|
108
|
-
"Pass again=true to deliberately
|
|
109
|
-
"
|
|
110
|
-
"Returns one outcome per file: '
|
|
173
|
+
"Make a BitGraph of one or more files. For each file, on this machine: hash it (the origin), allocate an unused slot in the BitGraph ledger (bitgraph.ing) before any artifact exists, build a new fused artifact from the file in memory with a registered placement (a 48-byte trailer for formats that ignore trailing bytes such as JPEG, PNG, TIFF, WebP; a small tar container otherwise), hash it, and commit that digest under the same slot. " +
|
|
174
|
+
"The file is never modified and never uploaded; only digests and slot records leave the machine. The fused bytes are not kept: the original plus the proof rebuilds them, and the Frame for each file is returned in the structured result. " +
|
|
175
|
+
"Files whose bytes are already on record, as a recording or as the origin of a fused artifact, are NOT BitGraphed again by default; they come back as 'on record' with their earliest position. " +
|
|
176
|
+
"Pass again=true to deliberately make a new fused artifact from a file already on record. " +
|
|
177
|
+
"BitGraphs are permanent: the ledger has 10-year retention and no deletes, so only BitGraph files the user asked to. " +
|
|
178
|
+
"Returns one outcome per file: 'fused' (a new fused artifact, with its placement, position and proof page URL), 'on record', or 'not fused' (with the error). " +
|
|
111
179
|
"Use bitgraph_check instead when the user only wants to know whether a file is on record.",
|
|
112
180
|
inputSchema: {
|
|
113
181
|
paths: z
|
|
114
182
|
.array(z.string().min(1))
|
|
115
183
|
.min(1)
|
|
116
184
|
.max(MAX_FILES)
|
|
117
|
-
.describe(`File paths to
|
|
118
|
-
attribution: z
|
|
119
|
-
.object({
|
|
120
|
-
name: z.string().max(200).optional().describe("Submitter's name (self-attributed)."),
|
|
121
|
-
title: z.string().max(200).optional(),
|
|
122
|
-
message: z.string().max(2000).optional(),
|
|
123
|
-
})
|
|
124
|
-
.optional()
|
|
125
|
-
.describe(
|
|
126
|
-
"Optional self-attributed submitter's note, stored in the signed proof. Rendered as a note, never as verified identity."
|
|
127
|
-
),
|
|
185
|
+
.describe(`File paths to BitGraph (absolute paths preferred), up to ${MAX_FILES}.`),
|
|
128
186
|
again: z
|
|
129
187
|
.boolean()
|
|
130
188
|
.default(false)
|
|
131
189
|
.describe(
|
|
132
|
-
"false (default): files already on record are returned as-is, nothing minted. true:
|
|
190
|
+
"false (default): files already on record are returned as-is, nothing minted. true: make a new fused artifact from every file even if its bytes are already on record. Outcomes are per unique file content: two paths with identical bytes yield one artifact."
|
|
133
191
|
),
|
|
134
192
|
response_format: responseFormatSchema,
|
|
135
193
|
},
|
|
@@ -140,142 +198,106 @@ export function buildServer(): McpServer {
|
|
|
140
198
|
openWorldHint: true,
|
|
141
199
|
},
|
|
142
200
|
},
|
|
143
|
-
async ({ paths,
|
|
201
|
+
async ({ paths, again, response_format }) => {
|
|
144
202
|
const config = configFromEnv();
|
|
145
203
|
try {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
byDigest.set(d, list);
|
|
204
|
+
const read = await readPaths(paths);
|
|
205
|
+
// Unique by content, first path wins for the artifact's name; extra paths listed too.
|
|
206
|
+
const byDigest = new Map<string, { bytes: Uint8Array; paths: string[] }>();
|
|
207
|
+
read.forEach((r, i) => {
|
|
208
|
+
const entry = byDigest.get(r.digestB64) ?? { bytes: r.bytes, paths: [] };
|
|
209
|
+
entry.paths.push(paths[i] as string);
|
|
210
|
+
byDigest.set(r.digestB64, entry);
|
|
154
211
|
});
|
|
155
212
|
const unique = [...byDigest.keys()];
|
|
156
|
-
|
|
157
213
|
const checked = await batchCheck(config, unique.map(toUrlSafeB64));
|
|
158
214
|
const existing = new Map<string, Array<{ proof: BitGraphProof }>>();
|
|
159
215
|
for (const d of unique) {
|
|
160
216
|
const entry = checked.results[toUrlSafeB64(d)];
|
|
161
217
|
if (entry && entry.proofs.length > 0) existing.set(d, entry.proofs);
|
|
162
218
|
}
|
|
163
|
-
|
|
164
219
|
const toMint = again ? unique : unique.filter((d) => !existing.has(d));
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
220
|
+
const fused = new Map<string, FusedSummary>();
|
|
221
|
+
const failed = new Map<string, string>();
|
|
222
|
+
for (const d of toMint) {
|
|
223
|
+
const entry = byDigest.get(d) as { bytes: Uint8Array; paths: string[] };
|
|
224
|
+
const name = (entry.paths[0] as string).split(/[\\/]/).pop() ?? "file";
|
|
169
225
|
try {
|
|
170
|
-
|
|
171
|
-
// A 200 with fewer proofs than digests is still a partial failure;
|
|
172
|
-
// never let it reach the success path looking complete.
|
|
173
|
-
if (minted.length < toMint.length) {
|
|
174
|
-
partial = new PartialCommitError(
|
|
175
|
-
minted,
|
|
176
|
-
toMint.length,
|
|
177
|
-
new Error("commit returned fewer proofs than digests sent")
|
|
178
|
-
);
|
|
179
|
-
}
|
|
226
|
+
fused.set(d, await fuseFile(entry.bytes, name, config));
|
|
180
227
|
} catch (err) {
|
|
181
|
-
|
|
182
|
-
minted = err.minted;
|
|
183
|
-
partial = err;
|
|
184
|
-
} else {
|
|
185
|
-
throw err;
|
|
186
|
-
}
|
|
228
|
+
failed.set(d, err instanceof Error ? err.message : String(err));
|
|
187
229
|
}
|
|
188
230
|
}
|
|
189
|
-
|
|
190
|
-
const mintedByDigest = new Map<string, BitGraphProof>();
|
|
191
|
-
for (const p of minted) {
|
|
192
|
-
const d = p.artifact?.digestB64;
|
|
193
|
-
if (d !== undefined) mintedByDigest.set(d, p);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
231
|
const outcomes: RecordOutcome[] = [];
|
|
197
|
-
|
|
198
|
-
|
|
232
|
+
const frames: Record<string, unknown> = {};
|
|
233
|
+
for (const [digest, entry] of byDigest) {
|
|
234
|
+
const made = fused.get(digest);
|
|
199
235
|
const prior = existing.get(digest);
|
|
200
|
-
for (const path of
|
|
201
|
-
if (
|
|
202
|
-
const { counter, epoch } = positionOf(
|
|
236
|
+
for (const path of entry.paths) {
|
|
237
|
+
if (made) {
|
|
238
|
+
const { counter, epoch } = positionOf(made.proof);
|
|
239
|
+
frames[toUrlSafeB64(made.artifactDigestB64)] = made.frame;
|
|
203
240
|
outcomes.push({
|
|
204
241
|
path,
|
|
205
242
|
digest: toUrlSafeB64(digest),
|
|
206
|
-
outcome: "
|
|
243
|
+
outcome: "fused",
|
|
244
|
+
artifact_digest: toUrlSafeB64(made.artifactDigestB64),
|
|
245
|
+
placement: made.placement,
|
|
207
246
|
counter,
|
|
208
247
|
epoch,
|
|
209
248
|
total_positions: (prior?.length ?? 0) + 1,
|
|
210
|
-
proof_url: proofUrl(
|
|
211
|
-
config.baseUrl,
|
|
212
|
-
digest,
|
|
213
|
-
counter ?? undefined,
|
|
214
|
-
mintedProof.commit?.epochId
|
|
215
|
-
),
|
|
249
|
+
proof_url: proofUrl(config.baseUrl, made.artifactDigestB64, counter ?? undefined, made.proof.commit?.epochId),
|
|
216
250
|
});
|
|
217
|
-
} else if (prior) {
|
|
251
|
+
} else if (prior && !failed.has(digest)) {
|
|
218
252
|
const first = prior[0]?.proof;
|
|
219
253
|
const { counter, epoch } = first ? positionOf(first) : { counter: null, epoch: null };
|
|
220
254
|
outcomes.push({
|
|
221
255
|
path,
|
|
222
256
|
digest: toUrlSafeB64(digest),
|
|
223
257
|
outcome: "on record",
|
|
258
|
+
artifact_digest: null,
|
|
259
|
+
placement: null,
|
|
224
260
|
counter,
|
|
225
261
|
epoch,
|
|
226
262
|
total_positions: prior.length,
|
|
227
263
|
proof_url: proofUrl(config.baseUrl, digest),
|
|
228
264
|
});
|
|
229
265
|
} else {
|
|
230
|
-
// Neither minted nor previously on record: lost to a partial
|
|
231
|
-
// failure. The honest outcome is "not recorded", never a claim.
|
|
232
266
|
outcomes.push({
|
|
233
267
|
path,
|
|
234
268
|
digest: toUrlSafeB64(digest),
|
|
235
|
-
outcome: "not
|
|
269
|
+
outcome: "not fused",
|
|
270
|
+
artifact_digest: null,
|
|
271
|
+
placement: null,
|
|
236
272
|
counter: null,
|
|
237
273
|
epoch: null,
|
|
238
|
-
total_positions: 0,
|
|
274
|
+
total_positions: prior?.length ?? 0,
|
|
239
275
|
proof_url: null,
|
|
276
|
+
error: failed.get(digest) ?? "not attempted",
|
|
240
277
|
});
|
|
241
278
|
}
|
|
242
279
|
}
|
|
243
280
|
}
|
|
244
|
-
|
|
245
281
|
const structured = {
|
|
246
282
|
results: outcomes as unknown as Record<string, unknown>[],
|
|
283
|
+
frames,
|
|
247
284
|
summary: {
|
|
248
|
-
|
|
285
|
+
fused: outcomes.filter((o) => o.outcome === "fused").length,
|
|
249
286
|
on_record: outcomes.filter((o) => o.outcome === "on record").length,
|
|
250
|
-
|
|
287
|
+
not_fused: outcomes.filter((o) => o.outcome === "not fused").length,
|
|
251
288
|
},
|
|
252
289
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
? `Some 'not recorded' files MAY still have been recorded server-side if the failure was a timeout. Run bitgraph_check on the 'not recorded' paths first, then re-run bitgraph_record with again=true for only the paths still missing.`
|
|
258
|
-
: `Re-run bitgraph_record with the same paths: already-recorded files will come back as 'on record' and only the missing ones will mint.`;
|
|
290
|
+
if (failed.size > 0) {
|
|
291
|
+
const guidance = again
|
|
292
|
+
? "A file marked 'not fused' MAY still have been BitGraphed if the failure was a timeout: run bitgraph_check on it first, then re-run bitgraph_record with again=true for only the files still missing."
|
|
293
|
+
: "Re-run bitgraph_record with the same paths: files already on record come back as 'on record' and only the missing ones are BitGraphed.";
|
|
259
294
|
return {
|
|
260
295
|
isError: true,
|
|
261
|
-
content: [
|
|
262
|
-
{
|
|
263
|
-
type: "text",
|
|
264
|
-
text:
|
|
265
|
-
`${errorText(partial.cause2)}\n` +
|
|
266
|
-
`${minted.length} of ${toMint.length} digests were recorded before the failure (those recordings are permanent); ${unrecorded} were not. ` +
|
|
267
|
-
`${retryGuidance}\n\n` +
|
|
268
|
-
renderRecordMarkdown(outcomes),
|
|
269
|
-
},
|
|
270
|
-
],
|
|
296
|
+
content: [{ type: "text", text: `${fused.size} of ${toMint.length} files were BitGraphed; ${failed.size} failed. ${guidance}\n\n${renderRecordMarkdown(outcomes)}` }],
|
|
271
297
|
structuredContent: structured,
|
|
272
298
|
};
|
|
273
299
|
}
|
|
274
|
-
|
|
275
|
-
const text =
|
|
276
|
-
response_format === "json"
|
|
277
|
-
? capJson(structured).text
|
|
278
|
-
: renderRecordMarkdown(outcomes);
|
|
300
|
+
const text = response_format === "json" ? capJson(structured).text : renderRecordMarkdown(outcomes);
|
|
279
301
|
return ok(text, structured);
|
|
280
302
|
} catch (err) {
|
|
281
303
|
return fail(errorText(err));
|
|
@@ -290,8 +312,8 @@ export function buildServer(): McpServer {
|
|
|
290
312
|
description:
|
|
291
313
|
"Check whether files or digests are on record in the BitGraph ledger, without recording anything. " +
|
|
292
314
|
"Accepts file paths (hashed locally; only digests are sent) and/or raw SHA-256 digests in standard or URL-safe base64. " +
|
|
293
|
-
"Returns, per item: on record or
|
|
294
|
-
"Read-only. Use bitgraph_record to
|
|
315
|
+
"Returns, per item: on_record (the bytes are on record, as an exact recording or as the original a new file was made from), every position by counter, and the proof page URL. " +
|
|
316
|
+
"Read-only. Use bitgraph_record to BitGraph files that turn out not to be on record.",
|
|
295
317
|
inputSchema: {
|
|
296
318
|
paths: z
|
|
297
319
|
.array(z.string().min(1))
|
|
@@ -346,8 +368,8 @@ export function buildServer(): McpServer {
|
|
|
346
368
|
input: input.label,
|
|
347
369
|
digest: toUrlSafeB64(input.standardDigest),
|
|
348
370
|
// A fused descendant that names these bytes as origin is not a recording of them.
|
|
349
|
-
|
|
350
|
-
|
|
371
|
+
// The original and the new file made from it find the same proof.
|
|
372
|
+
on_record: proofs.length > 0,
|
|
351
373
|
positions,
|
|
352
374
|
proof_url: proofs.length > 0 ? proofUrl(config.baseUrl, input.standardDigest) : null,
|
|
353
375
|
};
|