@mikeargento/bitgraph-mcp 0.1.1 → 0.2.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 +6 -4
- 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/server.d.ts +22 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +114 -85
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.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 -107
- package/src/types.ts +1 -1
|
@@ -15,7 +15,7 @@ import { tmpdir } from "node:os";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
17
17
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
18
|
-
import { buildServer } from "../server.js";
|
|
18
|
+
import { buildServer, type FusedSummary } from "../server.js";
|
|
19
19
|
import { toUrlSafeB64 } from "../encoding.js";
|
|
20
20
|
|
|
21
21
|
interface Recorded {
|
|
@@ -32,7 +32,6 @@ let fileB = "";
|
|
|
32
32
|
let fileC = "";
|
|
33
33
|
let digestA = ""; // standard b64 of fileA bytes
|
|
34
34
|
let digestB = "";
|
|
35
|
-
let commitMode: "ok" | "429" | "short" = "ok";
|
|
36
35
|
const EPOCH = createHash("sha256").update("test-epoch").digest("base64");
|
|
37
36
|
let mintCounter = 100;
|
|
38
37
|
|
|
@@ -90,17 +89,8 @@ before(async () => {
|
|
|
90
89
|
}
|
|
91
90
|
send(200, { results });
|
|
92
91
|
} else if (url.pathname === "/api/commit") {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
res.end(JSON.stringify({ error: "Rate limit exceeded: per-client proof rate limit exceeded" }));
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const digests = (body as { digests: Array<{ digestB64: string }> }).digests;
|
|
99
|
-
if (commitMode === "short") {
|
|
100
|
-
send(200, digests.slice(1).map((d) => proofFor(d.digestB64)));
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
send(200, digests.map((d) => proofFor(d.digestB64)));
|
|
92
|
+
// Ordinary recording is no longer what the record tool does; nothing here should call it.
|
|
93
|
+
send(500, { error: "unexpected /api/commit" });
|
|
104
94
|
} else if (url.pathname.startsWith("/api/proofs/digest/")) {
|
|
105
95
|
const digest = decodeURIComponent(url.pathname.split("/").pop() ?? "");
|
|
106
96
|
if (digest === toUrlSafeB64(digestA)) {
|
|
@@ -143,8 +133,24 @@ after(() => {
|
|
|
143
133
|
delete process.env["BITGRAPH_API_KEY"];
|
|
144
134
|
});
|
|
145
135
|
|
|
136
|
+
/** A stand-in for the fuse pipeline: no slot, no enclave; records what it was asked to fuse. */
|
|
137
|
+
const fusedCalls: Array<{ name: string; digestB64: string }> = [];
|
|
138
|
+
let fuseMode: "ok" | "fail" = "ok";
|
|
139
|
+
async function fakeFuse(bytes: Uint8Array, name: string): Promise<FusedSummary> {
|
|
140
|
+
const digestB64 = createHash("sha256").update(bytes).digest("base64");
|
|
141
|
+
fusedCalls.push({ name, digestB64 });
|
|
142
|
+
if (fuseMode === "fail") throw new Error("the boundary is restarting");
|
|
143
|
+
const artifactDigestB64 = createHash("sha256").update("fused:" + digestB64).digest("base64");
|
|
144
|
+
return {
|
|
145
|
+
proof: proofFor(artifactDigestB64) as unknown as FusedSummary["proof"],
|
|
146
|
+
frame: { type: "bitgraph-fuse/1" },
|
|
147
|
+
placement: "container/1",
|
|
148
|
+
artifactDigestB64,
|
|
149
|
+
originDigestB64: digestB64,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
146
152
|
async function connectedClient(): Promise<Client> {
|
|
147
|
-
const server = buildServer();
|
|
153
|
+
const server = buildServer({ fuseFile: fakeFuse });
|
|
148
154
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
149
155
|
const client = new Client({ name: "test-client", version: "0.0.0" });
|
|
150
156
|
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
|
|
@@ -158,56 +164,53 @@ test("lists the three tools", async () => {
|
|
|
158
164
|
assert.deepEqual(names, ["bitgraph_check", "bitgraph_get_proof", "bitgraph_record"]);
|
|
159
165
|
});
|
|
160
166
|
|
|
161
|
-
test("record dedups on-record files and
|
|
167
|
+
test("record dedups on-record files and fuses only fresh ones", async () => {
|
|
162
168
|
const client = await connectedClient();
|
|
163
169
|
requests.length = 0;
|
|
170
|
+
fusedCalls.length = 0;
|
|
164
171
|
const result = await client.callTool({
|
|
165
172
|
name: "bitgraph_record",
|
|
166
173
|
arguments: { paths: [fileA, fileB] },
|
|
167
174
|
});
|
|
168
175
|
assert.ok(!result.isError, JSON.stringify(result.content));
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const commitBody = commit.body as { digests: Array<{ digestB64: string; hashAlg: string }>; chainId: string };
|
|
173
|
-
assert.equal(commitBody.chainId, "bitgraph:main");
|
|
174
|
-
assert.deepEqual(commitBody.digests, [{ digestB64: digestB, hashAlg: "sha256" }], "only fileB minted, standard b64");
|
|
175
|
-
assert.equal(commit.headers["authorization"], "Bearer test-key-123", "api key forwarded");
|
|
176
|
-
|
|
176
|
+
assert.equal(requests.filter((r) => r.path === "/api/commit").length, 0, "nothing is committed as bytes-only");
|
|
177
|
+
assert.deepEqual(fusedCalls.map((c) => c.digestB64), [digestB], "only fileB fused; fileA was on record");
|
|
178
|
+
assert.equal(fusedCalls[0]?.name, "b.txt", "the artifact is named after the file");
|
|
177
179
|
const batch = requests.find((r) => r.path === "/api/proofs/batch");
|
|
178
|
-
if (!batch) throw new Error("no batch check preceded the
|
|
180
|
+
if (!batch) throw new Error("no batch check preceded the fuse");
|
|
179
181
|
const batchBody = batch.body as { digests: string[] };
|
|
180
182
|
assert.ok(batchBody.digests.every((d) => !d.includes("+") && !d.includes("=")), "check uses url-safe digests");
|
|
181
|
-
|
|
182
183
|
const structured = result.structuredContent as {
|
|
183
|
-
results: Array<{ path: string; outcome: string; counter: string | null }>;
|
|
184
|
-
|
|
184
|
+
results: Array<{ path: string; outcome: string; counter: string | null; placement: string | null; artifact_digest: string | null }>;
|
|
185
|
+
frames: Record<string, unknown>;
|
|
186
|
+
summary: { fused: number; on_record: number; not_fused: number };
|
|
185
187
|
};
|
|
186
|
-
assert.equal(structured.summary.
|
|
188
|
+
assert.equal(structured.summary.fused, 1);
|
|
187
189
|
assert.equal(structured.summary.on_record, 1);
|
|
190
|
+
assert.equal(structured.summary.not_fused, 0);
|
|
188
191
|
const a = structured.results.find((r) => r.path === fileA);
|
|
189
192
|
const b = structured.results.find((r) => r.path === fileB);
|
|
190
193
|
assert.equal(a?.outcome, "on record");
|
|
191
194
|
assert.equal(a?.counter, "10", "on-record outcome reports the EARLIEST position");
|
|
192
|
-
assert.equal(b?.outcome, "
|
|
195
|
+
assert.equal(b?.outcome, "fused");
|
|
196
|
+
assert.equal(b?.placement, "container/1");
|
|
197
|
+
assert.ok(b?.artifact_digest && structured.frames[b.artifact_digest], "the Frame rides in the structured result under the fused digest");
|
|
193
198
|
});
|
|
194
199
|
|
|
195
|
-
test("record with again=true
|
|
200
|
+
test("record with again=true fuses even on-record files", async () => {
|
|
196
201
|
const client = await connectedClient();
|
|
197
|
-
|
|
202
|
+
fusedCalls.length = 0;
|
|
198
203
|
const result = await client.callTool({
|
|
199
204
|
name: "bitgraph_record",
|
|
200
205
|
arguments: { paths: [fileA], again: true },
|
|
201
206
|
});
|
|
202
207
|
assert.ok(!result.isError, JSON.stringify(result.content));
|
|
203
|
-
|
|
204
|
-
const commitBody = commit?.body as { digests: Array<{ digestB64: string }> };
|
|
205
|
-
assert.deepEqual(commitBody.digests.map((d) => d.digestB64), [digestA]);
|
|
208
|
+
assert.deepEqual(fusedCalls.map((c) => c.digestB64), [digestA]);
|
|
206
209
|
const structured = result.structuredContent as {
|
|
207
210
|
results: Array<{ outcome: string; total_positions: number }>;
|
|
208
211
|
};
|
|
209
|
-
assert.equal(structured.results[0]?.outcome, "
|
|
210
|
-
assert.equal(structured.results[0]?.total_positions, 3, "two prior positions plus the new
|
|
212
|
+
assert.equal(structured.results[0]?.outcome, "fused");
|
|
213
|
+
assert.equal(structured.results[0]?.total_positions, 3, "two prior positions plus the new artifact");
|
|
211
214
|
});
|
|
212
215
|
|
|
213
216
|
test("check reports positions without ever committing", async () => {
|
|
@@ -274,49 +277,29 @@ test("get_proof requires exactly one selector", async () => {
|
|
|
274
277
|
assert.ok(result.isError);
|
|
275
278
|
});
|
|
276
279
|
|
|
277
|
-
test("
|
|
280
|
+
test("a fuse failure labels the file 'not fused', never 'on record'", async () => {
|
|
278
281
|
const client = await connectedClient();
|
|
279
|
-
|
|
282
|
+
fuseMode = "fail";
|
|
280
283
|
try {
|
|
281
284
|
const result = await client.callTool({
|
|
282
285
|
name: "bitgraph_record",
|
|
283
286
|
arguments: { paths: [fileC] },
|
|
284
287
|
});
|
|
285
|
-
assert.ok(result.isError, "
|
|
288
|
+
assert.ok(result.isError, "a failure must be an error result");
|
|
286
289
|
const text = (result.content as Array<{ text: string }>)[0]?.text ?? "";
|
|
287
|
-
assert.ok(text.includes("Retry after 30 seconds"), text);
|
|
288
290
|
assert.ok(text.includes("0 of 1"), text);
|
|
291
|
+
assert.ok(text.includes("the boundary is restarting"), text);
|
|
289
292
|
const structured = result.structuredContent as {
|
|
290
|
-
results: Array<{ outcome: string; proof_url: string | null;
|
|
291
|
-
summary: {
|
|
293
|
+
results: Array<{ outcome: string; proof_url: string | null; error?: string }>;
|
|
294
|
+
summary: { fused: number; on_record: number; not_fused: number };
|
|
292
295
|
};
|
|
293
|
-
assert.equal(structured.results[0]?.outcome, "not
|
|
296
|
+
assert.equal(structured.results[0]?.outcome, "not fused");
|
|
294
297
|
assert.equal(structured.results[0]?.proof_url, null);
|
|
295
|
-
assert.equal(structured.summary.
|
|
296
|
-
assert.equal(structured.summary.
|
|
298
|
+
assert.equal(structured.summary.not_fused, 1);
|
|
299
|
+
assert.equal(structured.summary.fused, 0);
|
|
297
300
|
assert.equal(structured.summary.on_record, 0);
|
|
298
301
|
} finally {
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test("a 200 with fewer proofs than digests is treated as a partial failure", async () => {
|
|
304
|
-
const client = await connectedClient();
|
|
305
|
-
commitMode = "short";
|
|
306
|
-
try {
|
|
307
|
-
const result = await client.callTool({
|
|
308
|
-
name: "bitgraph_record",
|
|
309
|
-
arguments: { paths: [fileC] },
|
|
310
|
-
});
|
|
311
|
-
assert.ok(result.isError, "shortfall must not reach the success path");
|
|
312
|
-
const structured = result.structuredContent as {
|
|
313
|
-
results: Array<{ outcome: string }>;
|
|
314
|
-
};
|
|
315
|
-
assert.equal(structured.results[0]?.outcome, "not recorded");
|
|
316
|
-
const text = (result.content as Array<{ text: string }>)[0]?.text ?? "";
|
|
317
|
-
assert.ok(text.includes("fewer proofs than digests"), text);
|
|
318
|
-
} finally {
|
|
319
|
-
commitMode = "ok";
|
|
302
|
+
fuseMode = "ok";
|
|
320
303
|
}
|
|
321
304
|
});
|
|
322
305
|
|
|
@@ -330,5 +313,5 @@ test("record surfaces unreadable paths before any network call", async () => {
|
|
|
330
313
|
assert.ok(result.isError);
|
|
331
314
|
assert.equal(requests.length, 0, "no API call happened; nothing was minted");
|
|
332
315
|
const text = (result.content as Array<{ text: string }>)[0]?.text ?? "";
|
|
333
|
-
assert.ok(text.includes("nothing was
|
|
316
|
+
assert.ok(text.includes("nothing was BitGraphed"));
|
|
334
317
|
});
|
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");
|