aipou-mcp-server 0.4.0 → 0.6.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.
Files changed (66) hide show
  1. package/NOTICE +8 -0
  2. package/README.md +18 -0
  3. package/dist/bounded-input.d.ts +8 -0
  4. package/dist/bounded-input.js +21 -0
  5. package/dist/bounded-input.test.d.ts +1 -0
  6. package/dist/bounded-input.test.js +24 -0
  7. package/dist/claims.js +119 -31
  8. package/dist/doctor.d.ts +32 -0
  9. package/dist/doctor.js +112 -0
  10. package/dist/doctor.test.d.ts +1 -0
  11. package/dist/doctor.test.js +164 -0
  12. package/dist/evidence.d.ts +21 -0
  13. package/dist/evidence.js +14 -0
  14. package/dist/feedback.d.ts +17 -0
  15. package/dist/feedback.js +24 -0
  16. package/dist/identity.d.ts +3 -0
  17. package/dist/identity.js +25 -1
  18. package/dist/index.js +15121 -106
  19. package/dist/init.d.ts +8 -2
  20. package/dist/init.js +20 -7
  21. package/dist/init.test.js +18 -4
  22. package/dist/lock.js +47 -9
  23. package/dist/lock.test.d.ts +1 -0
  24. package/dist/lock.test.js +26 -0
  25. package/dist/observed-outcome-pilot.d.ts +16 -0
  26. package/dist/observed-outcome-pilot.js +59 -0
  27. package/dist/observed-outcome-pilot.test.d.ts +1 -0
  28. package/dist/observed-outcome-pilot.test.js +25 -0
  29. package/dist/paybox-link.d.ts +34 -0
  30. package/dist/paybox-link.js +52 -0
  31. package/dist/paybox-link.test.d.ts +1 -0
  32. package/dist/paybox-link.test.js +36 -0
  33. package/dist/private-json.d.ts +2 -0
  34. package/dist/private-json.js +39 -0
  35. package/dist/protocol.test.js +23 -0
  36. package/dist/receipt-metadata.d.ts +13 -0
  37. package/dist/receipt-metadata.js +28 -0
  38. package/dist/receipt-metadata.test.d.ts +1 -0
  39. package/dist/receipt-metadata.test.js +39 -0
  40. package/dist/receipt-verifier.d.ts +13 -0
  41. package/dist/receipt-verifier.js +125 -0
  42. package/dist/receipt-verifier.test.d.ts +1 -0
  43. package/dist/receipt-verifier.test.js +54 -0
  44. package/dist/receipts.js +11 -2
  45. package/dist/settlement-driver.d.ts +3 -0
  46. package/dist/settlement-driver.js +120 -0
  47. package/dist/settlement-driver.test.d.ts +1 -0
  48. package/dist/settlement-driver.test.js +78 -0
  49. package/dist/settlement-runner.d.ts +34 -0
  50. package/dist/settlement-runner.js +40 -0
  51. package/dist/settlement-runner.test.d.ts +1 -0
  52. package/dist/settlement-runner.test.js +128 -0
  53. package/dist/status.js +2 -0
  54. package/dist/store.d.ts +2 -0
  55. package/dist/store.js +33 -9
  56. package/dist/technocore-link.d.ts +34 -0
  57. package/dist/technocore-link.js +49 -0
  58. package/dist/technocore-link.test.d.ts +1 -0
  59. package/dist/technocore-link.test.js +36 -0
  60. package/dist/tool-metadata.test.d.ts +1 -0
  61. package/dist/tool-metadata.test.js +86 -0
  62. package/dist/types.d.ts +6 -0
  63. package/dist/usage-recovery.test.d.ts +1 -0
  64. package/dist/usage-recovery.test.js +121 -0
  65. package/package.json +7 -5
  66. package/server.json +2 -2
package/dist/store.js CHANGED
@@ -1,6 +1,9 @@
1
- import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { withFileLock } from "./lock.js";
4
+ import { isTaskSessionExpired } from "./identity.js";
5
+ import { canonicalJson } from "./canonical.js";
6
+ import { writePrivateJson } from "./private-json.js";
4
7
  const emptyState = () => ({ sessions: {}, receipts: [], completedEvidence: {}, batches: [] });
5
8
  let mutationQueue = Promise.resolve();
6
9
  function dataDir() {
@@ -26,9 +29,7 @@ async function readState() {
26
29
  }
27
30
  }
28
31
  async function writeState(state) {
29
- const temporary = `${statePath()}.${process.pid}.tmp`;
30
- await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
31
- await rename(temporary, statePath());
32
+ await writePrivateJson(statePath(), state);
32
33
  }
33
34
  // The in-process queue orders local callers; the file lock orders the multiple
34
35
  // MCP server processes (Claude Code, OpenClaw, Codex) that share one data dir.
@@ -63,7 +64,7 @@ export async function saveCompletedReceipt(evidenceKey, receipt) {
63
64
  if (!session)
64
65
  throw new Error("Unknown task nonce");
65
66
  if (session.completedAt)
66
- throw new Error("Task nonce has already been completed");
67
+ throw new Error("Task nonce has already been completed; recover its stored receipt with export_ai_receipts and match the nonce.");
67
68
  if (state.completedEvidence[evidenceKey]) {
68
69
  throw new Error(`Duplicate task/output evidence already recorded as ${state.completedEvidence[evidenceKey]}`);
69
70
  }
@@ -95,7 +96,13 @@ async function readArchivedReceipts() {
95
96
  return receipts;
96
97
  }
97
98
  export async function exportStoredReceipts(wallet) {
98
- const receipts = [...(await readArchivedReceipts()), ...(await readState()).receipts];
99
+ const unique = new Map();
100
+ for (const receipt of [...(await readState()).receipts, ...(await readArchivedReceipts())]) {
101
+ const prior = unique.get(receipt.receiptId);
102
+ if (!prior?.claimTransaction)
103
+ unique.set(receipt.receiptId, receipt);
104
+ }
105
+ const receipts = [...unique.values()];
99
106
  if (!wallet)
100
107
  return receipts;
101
108
  return receipts.filter((receipt) => receipt.wallet.toLowerCase() === wallet.toLowerCase());
@@ -110,15 +117,23 @@ export async function protocolStateCounts() {
110
117
  sessions: sessions.length,
111
118
  completedSessions: sessions.filter((session) => session.completedAt).length,
112
119
  incompleteSessions: sessions.filter((session) => !session.completedAt).length,
120
+ activeSessions: sessions.filter((session) => !session.completedAt && !isTaskSessionExpired(session)).length,
121
+ expiredSessions: sessions.filter((session) => !session.completedAt && isTaskSessionExpired(session)).length,
113
122
  batches: state.batches.length
114
123
  };
115
124
  }
116
125
  export async function settledReceiptsSince(sinceMs) {
117
- const receipts = [...(await readArchivedReceipts()), ...(await readState()).receipts];
126
+ const receipts = await exportStoredReceipts();
118
127
  return receipts.filter((receipt) => receipt.claimTransaction && Date.parse(receipt.recordedAt) >= sinceMs);
119
128
  }
120
129
  export async function markBatchSettled(batch) {
121
130
  await mutate(async (state) => {
131
+ const existing = state.batches.find(item => item.root === batch.root);
132
+ if (existing) {
133
+ if (existing.claimTransaction !== batch.claimTransaction)
134
+ throw new Error("Settlement archive transaction mismatch");
135
+ return;
136
+ }
122
137
  const included = new Set(batch.receiptIds);
123
138
  const settled = [];
124
139
  for (const receipt of state.receipts) {
@@ -133,8 +148,17 @@ export async function markBatchSettled(batch) {
133
148
  // Settled receipts move to per-batch archive files so state.json stays
134
149
  // small; their ids stay in state to keep the replay check O(state).
135
150
  await mkdir(archiveDir(), { recursive: true });
136
- const archivePath = path.join(archiveDir(), `batch-${batch.root.slice(2, 18)}.json`);
137
- await writeFile(archivePath, `${JSON.stringify(settled, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
151
+ const archivePath = path.join(archiveDir(), `batch-${batch.root.slice(2)}.json`);
152
+ try {
153
+ const prior = JSON.parse(await readFile(archivePath, "utf8"));
154
+ if (canonicalJson(prior) !== canonicalJson(settled))
155
+ throw new Error("Settlement archive does not match saved batch");
156
+ }
157
+ catch (error) {
158
+ if (error.code !== "ENOENT")
159
+ throw error;
160
+ await writePrivateJson(archivePath, settled);
161
+ }
138
162
  state.receipts = state.receipts.filter((receipt) => !included.has(receipt.receiptId));
139
163
  state.settledReceiptIds = [...(state.settledReceiptIds ?? []), ...batch.receiptIds];
140
164
  state.batches.push(batch);
@@ -0,0 +1,34 @@
1
+ export declare const TECHNOCORE_TRANSPORT_SCHEME = "technocore-room-receipt-v1";
2
+ export declare const EXTERNAL_EVIDENCE_LINK_SCHEME = "external-evidence-link-v1";
3
+ export type TechnocoreWorkLinkInput = {
4
+ workReceiptId: string;
5
+ transportArtifactId: string;
6
+ transportArtifactDigest: string;
7
+ issuedAt: string;
8
+ };
9
+ export type TechnocoreWorkLink = {
10
+ scheme: typeof EXTERNAL_EVIDENCE_LINK_SCHEME;
11
+ /** Stable logical identity that excludes issuedAt for retry-safe deduplication. */
12
+ linkId: string;
13
+ relation: "supports";
14
+ source: {
15
+ kind: "receipt";
16
+ scheme: "aipou-receipt-v1";
17
+ id: string;
18
+ digest: string;
19
+ };
20
+ target: {
21
+ kind: "transport-record";
22
+ scheme: typeof TECHNOCORE_TRANSPORT_SCHEME;
23
+ id: string;
24
+ digest: string;
25
+ };
26
+ privacy: "digest_only";
27
+ issuedAt: string;
28
+ linkDigest: string;
29
+ };
30
+ /**
31
+ * Creates a digest-only correlation artifact. It never calls Technocore or
32
+ * upgrades a supplied snapshot into verified signed transport evidence.
33
+ */
34
+ export declare function createTechnocoreWorkLink(input: TechnocoreWorkLinkInput): TechnocoreWorkLink;
@@ -0,0 +1,49 @@
1
+ import { canonicalJson, sha256Hex } from "./canonical.js";
2
+ export const TECHNOCORE_TRANSPORT_SCHEME = "technocore-room-receipt-v1";
3
+ export const EXTERNAL_EVIDENCE_LINK_SCHEME = "external-evidence-link-v1";
4
+ const receiptIdPattern = /^0x[0-9a-f]{64}$/;
5
+ const sha256Pattern = /^sha256:[0-9a-f]{64}$/;
6
+ function assertValidInput(input) {
7
+ if (!receiptIdPattern.test(input.workReceiptId)) {
8
+ throw new Error("workReceiptId must be a lowercase 0x-prefixed SHA-256 identifier");
9
+ }
10
+ if (!sha256Pattern.test(input.transportArtifactDigest)) {
11
+ throw new Error("transportArtifactDigest must be a lowercase sha256 digest");
12
+ }
13
+ if (input.transportArtifactId.length < 1 || input.transportArtifactId.length > 256) {
14
+ throw new Error("transportArtifactId must be an opaque reference between 1 and 256 characters");
15
+ }
16
+ if (!Number.isFinite(Date.parse(input.issuedAt))) {
17
+ throw new Error("issuedAt must be an ISO-8601 timestamp");
18
+ }
19
+ }
20
+ /**
21
+ * Creates a digest-only correlation artifact. It never calls Technocore or
22
+ * upgrades a supplied snapshot into verified signed transport evidence.
23
+ */
24
+ export function createTechnocoreWorkLink(input) {
25
+ assertValidInput(input);
26
+ const identity = {
27
+ scheme: EXTERNAL_EVIDENCE_LINK_SCHEME,
28
+ relation: "supports",
29
+ source: {
30
+ kind: "receipt",
31
+ scheme: "aipou-receipt-v1",
32
+ id: input.workReceiptId,
33
+ digest: `sha256:${input.workReceiptId.slice(2)}`
34
+ },
35
+ target: {
36
+ kind: "transport-record",
37
+ scheme: TECHNOCORE_TRANSPORT_SCHEME,
38
+ id: input.transportArtifactId,
39
+ digest: input.transportArtifactDigest
40
+ },
41
+ privacy: "digest_only"
42
+ };
43
+ const payload = { ...identity, issuedAt: input.issuedAt };
44
+ return {
45
+ ...payload,
46
+ linkId: `sha256:${sha256Hex(canonicalJson(identity)).slice(2)}`,
47
+ linkDigest: `sha256:${sha256Hex(canonicalJson(payload)).slice(2)}`
48
+ };
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createTechnocoreWorkLink } from "./technocore-link.js";
4
+ const input = {
5
+ workReceiptId: `0x${"ab".repeat(32)}`,
6
+ transportArtifactId: "technocore:lobby:receipt-synthetic-001",
7
+ transportArtifactDigest: `sha256:${"cd".repeat(32)}`,
8
+ issuedAt: "2026-08-26T14:30:00.000Z"
9
+ };
10
+ test("creates a digest-only Technocore to AIPOU work link without transport authority", () => {
11
+ const link = createTechnocoreWorkLink(input);
12
+ assert.equal(link.scheme, "external-evidence-link-v1");
13
+ assert.equal(link.relation, "supports");
14
+ assert.equal(link.source.id, input.workReceiptId);
15
+ assert.equal(link.target.scheme, "technocore-room-receipt-v1");
16
+ assert.equal(link.target.id, input.transportArtifactId);
17
+ assert.equal(link.target.digest, input.transportArtifactDigest);
18
+ assert.match(link.linkId, /^sha256:[0-9a-f]{64}$/);
19
+ assert.match(link.linkDigest, /^sha256:[0-9a-f]{64}$/);
20
+ assert.equal("did" in link, false);
21
+ assert.equal("signature" in link, false);
22
+ assert.equal("deliveryStatus" in link, false);
23
+ assert.equal("claimStatus" in link, false);
24
+ });
25
+ test("is deterministic and changes when the transport artifact changes", () => {
26
+ assert.equal(createTechnocoreWorkLink(input).linkDigest, createTechnocoreWorkLink(input).linkDigest);
27
+ assert.equal(createTechnocoreWorkLink(input).linkId, createTechnocoreWorkLink({ ...input, issuedAt: "2026-08-26T15:30:00.000Z" }).linkId);
28
+ assert.notEqual(createTechnocoreWorkLink(input).linkDigest, createTechnocoreWorkLink({ ...input, issuedAt: "2026-08-26T15:30:00.000Z" }).linkDigest);
29
+ assert.notEqual(createTechnocoreWorkLink(input).linkId, createTechnocoreWorkLink({ ...input, transportArtifactDigest: `sha256:${"ef".repeat(32)}` }).linkId);
30
+ });
31
+ test("rejects malformed receipt, digest, opaque reference, and timestamp inputs", () => {
32
+ assert.throws(() => createTechnocoreWorkLink({ ...input, workReceiptId: "0xabc" }));
33
+ assert.throws(() => createTechnocoreWorkLink({ ...input, transportArtifactDigest: "sha256:ABC" }));
34
+ assert.throws(() => createTechnocoreWorkLink({ ...input, transportArtifactId: "" }));
35
+ assert.throws(() => createTechnocoreWorkLink({ ...input, issuedAt: "not-a-date" }));
36
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
+ test("tool metadata explains parameters, side effects, and safe alternatives", async () => {
7
+ const transport = new StdioClientTransport({
8
+ command: process.execPath,
9
+ args: [fileURLToPath(new URL("./index.js", import.meta.url))],
10
+ stderr: "pipe"
11
+ });
12
+ const client = new Client({ name: "aipou-tool-metadata-test", version: "1.0.0" });
13
+ await client.connect(transport);
14
+ try {
15
+ const { tools } = await client.listTools();
16
+ const complete = tools.find((tool) => tool.name === "complete_ai_task");
17
+ const estimate = tools.find((tool) => tool.name === "estimate_ai_reward");
18
+ const status = tools.find((tool) => tool.name === "get_aipou_status");
19
+ const begin = tools.find((tool) => tool.name === "begin_ai_task");
20
+ const paybox = tools.find((tool) => tool.name === "create_paybox_work_link");
21
+ const technocore = tools.find((tool) => tool.name === "create_technocore_work_link");
22
+ const verifyReceipt = tools.find((tool) => tool.name === "verify_aipou_receipt");
23
+ const settleAll = tools.find((tool) => tool.name === "settle_all_ai_rewards");
24
+ assert.ok(complete);
25
+ assert.match(complete.description ?? "", /once after begin_ai_task/i);
26
+ assert.match(complete.description ?? "", /writes the local receipt store/i);
27
+ assert.match(complete.description ?? "", /does not .*submit a blockchain transaction/i);
28
+ assert.match(complete.description ?? "", /workReceiptId/i);
29
+ assert.match(complete.description ?? "", /get_aipou_status/i);
30
+ assert.deepEqual(complete.annotations, {
31
+ readOnlyHint: false,
32
+ destructiveHint: false,
33
+ idempotentHint: false,
34
+ openWorldHint: false
35
+ });
36
+ const completeProperties = complete.inputSchema.properties;
37
+ for (const field of ["nonce", "inputTokens", "outputTokens", "durationSeconds", "outputHash"]) {
38
+ assert.ok(completeProperties[field]?.description, `${field} must have a description`);
39
+ }
40
+ assert.ok(completeProperties.providerEvidence?.description);
41
+ assert.ok(completeProperties.providerEvidence?.properties?.keyId?.description);
42
+ assert.ok(completeProperties.providerEvidence?.properties?.signature?.description);
43
+ assert.ok(estimate);
44
+ assert.match(estimate.description ?? "", /informational/i);
45
+ assert.match(estimate.description ?? "", /inputTokens, outputTokens, and durationSeconds/i);
46
+ assert.equal(estimate.annotations?.readOnlyHint, true);
47
+ assert.equal(estimate.annotations?.idempotentHint, true);
48
+ assert.ok(status);
49
+ assert.match(status.description ?? "", /read-only JSON summary/i);
50
+ assert.match(status.description ?? "", /never signs or submits a transaction/i);
51
+ assert.match(status.description ?? "", /explicit claim request/i);
52
+ assert.equal(status.annotations?.readOnlyHint, true);
53
+ assert.equal(status.annotations?.openWorldHint, true);
54
+ assert.ok(begin);
55
+ assert.match(begin.description ?? "", /writes a pending local task session/i);
56
+ assert.match(begin.description ?? "", /does not publish, mint, or submit an on-chain transaction/i);
57
+ assert.equal(begin.annotations?.readOnlyHint, false);
58
+ assert.equal(begin.annotations?.idempotentHint, false);
59
+ const beginProperties = begin.inputSchema.properties;
60
+ for (const field of ["provider", "model", "taskHash", "client"]) {
61
+ assert.ok(beginProperties[field]?.description, `${field} must have a description`);
62
+ }
63
+ for (const tool of [paybox, technocore]) {
64
+ assert.ok(tool);
65
+ assert.match(tool.description ?? "", /pure local calculation/i);
66
+ assert.match(tool.description ?? "", /does not persist/i);
67
+ assert.equal(tool.annotations?.readOnlyHint, true);
68
+ assert.equal(tool.annotations?.idempotentHint, true);
69
+ }
70
+ assert.ok(verifyReceipt);
71
+ assert.match(verifyReceipt.description ?? "", /self-contained integrity/i);
72
+ assert.match(verifyReceipt.description ?? "", /does not .*prove delivery/i);
73
+ assert.equal(verifyReceipt.annotations?.readOnlyHint, true);
74
+ assert.equal(verifyReceipt.annotations?.idempotentHint, true);
75
+ assert.ok(settleAll);
76
+ assert.match(settleAll.description ?? "", /explicit broad request/i);
77
+ assert.equal(settleAll.annotations?.readOnlyHint, false);
78
+ assert.equal(settleAll.annotations?.openWorldHint, true);
79
+ const settleAllProperties = settleAll.inputSchema.properties;
80
+ assert.ok(settleAllProperties.batchSize?.description);
81
+ assert.ok(settleAllProperties.maxBatches?.description);
82
+ }
83
+ finally {
84
+ await transport.close();
85
+ }
86
+ });
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type TrustTier = "client_signed" | "provider_signed";
2
+ export type TokenUsageMeasurement = "measured" | "unmeasured" | "unspecified";
2
3
  export interface TaskSession {
3
4
  nonce: string;
4
5
  wallet: string;
@@ -7,6 +8,8 @@ export interface TaskSession {
7
8
  taskHash: string;
8
9
  client: string;
9
10
  issuedAt: number;
11
+ /** Unix seconds. A pending session is not authority to complete after this point. */
12
+ expiresAt?: number;
10
13
  chainId: number;
11
14
  verifyingContract: string;
12
15
  walletAuthorization: string;
@@ -17,6 +20,7 @@ export interface ProviderEvidence {
17
20
  signature: string;
18
21
  }
19
22
  export interface CompleteTaskInput {
23
+ tokenUsageMeasurement?: TokenUsageMeasurement;
20
24
  nonce: string;
21
25
  inputTokens: number;
22
26
  outputTokens: number;
@@ -25,6 +29,8 @@ export interface CompleteTaskInput {
25
29
  providerEvidence?: ProviderEvidence;
26
30
  }
27
31
  export interface UsageReceipt {
32
+ /** Absent on legacy signed receipts; absence never implies measured usage. */
33
+ tokenUsageMeasurement?: TokenUsageMeasurement;
28
34
  receiptId: string;
29
35
  nonce: string;
30
36
  wallet: string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, mkdir, readFile, writeFile, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { Wallet } from "ethers";
7
+ import { canonicalJson, sha256Hex } from "./canonical.js";
8
+ import { signCollectorPayload } from "./collector.js";
9
+ import { localFeedbackSummary } from "./feedback.js";
10
+ import { beginTask, completeTask, exportReceipts } from "./receipts.js";
11
+ import { verifyAipouReceipt } from "./receipt-verifier.js";
12
+ import { markBatchSettled, unsettledReceipts } from "./store.js";
13
+ test("unmeasured zero differs from measured zero and recovered receipts are not duplicated", async () => {
14
+ const dir = await mkdtemp(path.join(tmpdir(), "aipou-usage-"));
15
+ process.env.AIPOU_DATA_DIR = dir;
16
+ process.env.AIPOU_AGENT_PRIVATE_KEY = Wallet.createRandom().privateKey;
17
+ try {
18
+ const create = (s) => beginTask({
19
+ provider: "local",
20
+ model: "test",
21
+ client: "test",
22
+ taskHash: sha256Hex(s),
23
+ chainId: 8453,
24
+ verifyingContract: Wallet.createRandom().address,
25
+ });
26
+ const one = await create("one");
27
+ await assert.rejects(completeTask({
28
+ nonce: one.nonce,
29
+ inputTokens: 1,
30
+ outputTokens: 0,
31
+ durationSeconds: 1,
32
+ outputHash: sha256Hex("out"),
33
+ tokenUsageMeasurement: "unmeasured",
34
+ }), /zero counts/);
35
+ const receipt = await completeTask({
36
+ nonce: one.nonce,
37
+ inputTokens: 0,
38
+ outputTokens: 0,
39
+ durationSeconds: 1,
40
+ outputHash: sha256Hex("out"),
41
+ tokenUsageMeasurement: "unmeasured",
42
+ });
43
+ assert.equal(receipt.tokenUsageMeasurement, "unmeasured");
44
+ assert.equal(verifyAipouReceipt(receipt).valid, true);
45
+ const { tokenUsageMeasurement: _measurement, receiptId: _id, collectorSignature: _signature, ...legacyUnsigned } = receipt;
46
+ const legacyId = sha256Hex(canonicalJson(legacyUnsigned));
47
+ const legacy = {
48
+ receiptId: legacyId,
49
+ ...legacyUnsigned,
50
+ collectorSignature: await signCollectorPayload({
51
+ receiptId: legacyId,
52
+ ...legacyUnsigned,
53
+ }),
54
+ };
55
+ assert.equal(verifyAipouReceipt(legacy).valid, true, "legacy receipts remain valid without the new optional field");
56
+ await assert.rejects(completeTask({
57
+ nonce: one.nonce,
58
+ inputTokens: 0,
59
+ outputTokens: 0,
60
+ durationSeconds: 1,
61
+ outputHash: receipt.outputHash,
62
+ }), /export_ai_receipts/);
63
+ assert.equal((await exportReceipts()).find((r) => r.nonce === one.nonce)?.receiptId, receipt.receiptId);
64
+ const two = await create("two");
65
+ const measured = await completeTask({
66
+ nonce: two.nonce,
67
+ inputTokens: 0,
68
+ outputTokens: 0,
69
+ durationSeconds: 1,
70
+ outputHash: sha256Hex("out2"),
71
+ tokenUsageMeasurement: "measured",
72
+ });
73
+ assert.equal(measured.tokenUsageMeasurement, "measured");
74
+ const three = await create("three");
75
+ const unspecified = await completeTask({
76
+ nonce: three.nonce,
77
+ inputTokens: 0,
78
+ outputTokens: 0,
79
+ durationSeconds: 1,
80
+ outputHash: sha256Hex("out3"),
81
+ });
82
+ assert.equal(unspecified.tokenUsageMeasurement, "unspecified");
83
+ const feedback = await localFeedbackSummary();
84
+ assert.equal(feedback.localReceiptCount, 3);
85
+ assert.equal(feedback.measuredUsageReceipts, 1);
86
+ assert.equal(feedback.unmeasuredUsageReceipts, 1);
87
+ assert.equal(feedback.unspecifiedUsageReceipts, 1);
88
+ const text = JSON.stringify(feedback);
89
+ assert.ok(!text.includes(receipt.receiptId));
90
+ assert.ok(!text.includes(receipt.wallet));
91
+ assert.ok(!text.includes(dir));
92
+ // Simulate archive written but state update interrupted; recovery is idempotent.
93
+ const batch = {
94
+ root: sha256Hex("batch"),
95
+ receiptIds: [receipt.receiptId],
96
+ publishTransaction: "0xpublish",
97
+ claimTransaction: "0xclaim",
98
+ settledAt: new Date().toISOString(),
99
+ };
100
+ await mkdir(path.join(dir, "settled"), { recursive: true });
101
+ await writeFile(path.join(dir, "settled", `batch-${batch.root.slice(2)}.json`), JSON.stringify([
102
+ {
103
+ ...receipt,
104
+ batchRoot: batch.root,
105
+ claimTransaction: batch.claimTransaction,
106
+ },
107
+ ]));
108
+ await markBatchSettled(batch);
109
+ await markBatchSettled(batch);
110
+ assert.equal((await unsettledReceipts(100)).length, 2);
111
+ assert.equal((await exportReceipts()).filter((r) => r.receiptId === receipt.receiptId)
112
+ .length, 1);
113
+ assert.equal(JSON.parse(await readFile(path.join(dir, "state.json"), "utf8")).batches
114
+ .length, 1);
115
+ }
116
+ finally {
117
+ await rm(dir, { recursive: true, force: true });
118
+ delete process.env.AIPOU_DATA_DIR;
119
+ delete process.env.AIPOU_AGENT_PRIVATE_KEY;
120
+ }
121
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipou-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "mcpName": "io.github.0xddneto/ai-proof-of-us",
5
5
  "description": "MCP server for private, signed AI task receipts and optional validated AIPOU claims.",
6
6
  "author": "0xddneto",
@@ -29,14 +29,15 @@
29
29
  "files": [
30
30
  "dist",
31
31
  "README.md",
32
+ "NOTICE",
32
33
  "server.json"
33
34
  ],
34
35
  "bin": {
35
36
  "aipou-mcp": "dist/index.js"
36
37
  },
37
38
  "scripts": {
38
- "build": "tsc -p tsconfig.json",
39
- "test": "npm run build && node --test dist/protocol.test.js dist/rewards.test.js dist/canonical.test.js dist/demo.test.js dist/init.test.js dist/version.test.js",
39
+ "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:dotenv --external:dotenv/* --external:ethers --external:merkletreejs --external:zod --external:zod/*",
40
+ "test": "npm run build && node --test dist/*.test.js",
40
41
  "pack:check": "npm test && npm pack --dry-run",
41
42
  "demo": "npm run build && node dist/index.js --demo",
42
43
  "dev": "tsx src/index.ts",
@@ -50,14 +51,15 @@
50
51
  "node": ">=20"
51
52
  },
52
53
  "dependencies": {
53
- "@modelcontextprotocol/sdk": "^1.13.0",
54
54
  "dotenv": "^16.6.1",
55
55
  "ethers": "^6.17.0",
56
56
  "merkletreejs": "^0.6.0",
57
- "zod": "^3.23.8"
57
+ "zod": "^4.6.7"
58
58
  },
59
59
  "devDependencies": {
60
+ "@modelcontextprotocol/sdk": "1.32.2",
60
61
  "@types/node": "^22.5.4",
62
+ "esbuild": "0.28.1",
61
63
  "tsx": "^4.19.0",
62
64
  "typescript": "^5.5.4"
63
65
  }
package/server.json CHANGED
@@ -9,12 +9,12 @@
9
9
  "source": "github",
10
10
  "subfolder": "mcp-server"
11
11
  },
12
- "version": "0.4.0",
12
+ "version": "0.6.0",
13
13
  "packages": [
14
14
  {
15
15
  "registryType": "npm",
16
16
  "identifier": "aipou-mcp-server",
17
- "version": "0.4.0",
17
+ "version": "0.6.0",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },