tersign 0.4.4 → 0.4.6

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.
@@ -8,6 +8,6 @@ export declare function envDeps(env?: Record<string, string | undefined>): McpDe
8
8
  * every client; mcp.test.ts pins it against package.json so a release bump can't drift it. */
9
9
  export declare const MCP_SERVER_IDENTITY: {
10
10
  readonly name: "tersign";
11
- readonly version: "0.4.4";
11
+ readonly version: "0.4.6";
12
12
  };
13
13
  export declare function buildServer(deps: McpDeps): McpServer;
@@ -3,14 +3,18 @@ import { z } from 'zod';
3
3
  import { privateKeyToAccount } from 'viem/accounts';
4
4
  import { Assure } from '../assure.js';
5
5
  import { LedgerClient } from '../ledgerClient.js';
6
+ import { resolveSignerKey } from '../keystore.js';
6
7
  import { adjudicateDisputeTool, getDisputeTool, issueReceiptTool, openDisputeTool, recordDisclosureTool, recordRefundTool, submitEvidenceTool, verifyReceiptTool, verifyRecordTool, } from './tools.js';
7
8
  /** MCP packaging: exposes assure as tools any MCP-speaking agent can call, so an agent
8
9
  * (or its framework) can issue, verify, and chain receipts without importing the SDK.
9
10
  * Config via env — see envDeps(). */
10
11
  export function envDeps(env = process.env) {
11
- const key = env.TERSIGN_SELLER_KEY;
12
- if (!key)
13
- throw new Error('TERSIGN_SELLER_KEY (0x-prefixed private key) is required');
12
+ // Same key resolution as the CLI surfaces: TERSIGN_SELLER_KEY, else the OS keychain, else a
13
+ // 0600 keyfile, else generate one and persist it. record_disclosure's own description promises
14
+ // that first call self-provisions a signer-keyed account; before this the MCP entry point threw
15
+ // instead, so `npx tersign` died on first run for anyone who had not already exported a key —
16
+ // and no directory or sandbox could introspect the server at all.
17
+ const key = env.TERSIGN_SELLER_KEY ?? resolveSignerKey({ create: true }).key;
14
18
  const account = privateKeyToAccount(key);
15
19
  const assure = new Assure({
16
20
  signer: account,
@@ -45,30 +49,47 @@ function json(value) {
45
49
  }
46
50
  /** MUST match package.json name/version — the MCP handshake self-reports this identity to
47
51
  * every client; mcp.test.ts pins it against package.json so a release bump can't drift it. */
48
- export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.4.4' };
52
+ export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.4.6' };
49
53
  export function buildServer(deps) {
50
54
  const server = new McpServer(MCP_SERVER_IDENTITY);
51
55
  server.registerTool('issue_receipt', {
52
56
  title: 'Issue signed receipt',
53
- description: 'Issue an x402 offer-receipt (EIP-712) plus an Tersign compliance record for a settled payment; counter-signs into the ledger when configured.',
57
+ description: 'Issue an x402 offer-receipt (EIP-712) plus a Tersign action record for a payment that has ALREADY settled, and counter-sign both into your hash chain when a ledger is configured. ' +
58
+ 'Use this for money that moved; use record_disclosure for a non-payment agent action. ' +
59
+ 'Side effects: signs with TERSIGN_SELLER_KEY, and performs ONE network write to the ledger when TERSIGN_LEDGER_URL/_API_KEY/_SELLER_ID are set (without them it signs locally and returns an unchained artifact). ' +
60
+ 'Returns the signed receipt artifact, its keccak256 canonical digest, and — when chained — the ledger counter-signature and sequence number.',
54
61
  inputSchema: {
55
- network: z.string().describe('CAIP-2, e.g. eip155:8453'),
56
- resourceUrl: z.string().url(),
57
- payer: z.string(),
58
- supplyDescription: z.string(),
59
- settledAt: z.number().int().optional(),
60
- txHash: z.string().optional(),
61
- taxScheme: z.enum(['none', 'vat', 'gst', 'jct', 'sales']).optional(),
62
- currency: z.string().optional(),
63
- principal: z.string().optional().describe('signed principal behind the paying agent'),
62
+ network: z.string().describe('settlement network as CAIP-2, e.g. "eip155:8453" for Base mainnet'),
63
+ resourceUrl: z.string().url().describe('absolute URL of the resource that was paid for; appears verbatim in the receipt'),
64
+ payer: z.string().describe('0x address that paid — the party who can later open a dispute against this receipt'),
65
+ supplyDescription: z.string().describe('what was supplied, in the seller\'s own words; the human-readable line an auditor or venue reads'),
66
+ settledAt: z.number().int().optional().describe('unix seconds when settlement occurred; defaults to now. Set it explicitly when back-filling'),
67
+ txHash: z.string().optional().describe('on-chain settlement transaction hash, when one exists; omit for off-chain or fiat settlement'),
68
+ taxScheme: z
69
+ .enum(['none', 'vat', 'gst', 'jct', 'sales'])
70
+ .optional()
71
+ .describe('tax regime the seller is accounting under; recorded, never computed — Tersign does not calculate tax'),
72
+ currency: z.string().optional().describe('settlement currency code, e.g. "USDC" or "USD"'),
73
+ principal: z
74
+ .string()
75
+ .optional()
76
+ .describe('the party on whose authority the paying agent acted (x402 sense: the buyer who delegated). Omit when a human paid directly'),
64
77
  },
65
78
  }, async (args) => json(await issueReceiptTool(deps, args)));
66
79
  server.registerTool('verify_receipt', {
67
80
  title: 'Verify signed receipt',
68
- description: 'Verify an offer-receipt artifact (EIP-712) and optionally enforce an expected signer (payTo authorization).',
81
+ description: 'Verify an offer-receipt artifact: recover the EIP-712 signature and confirm the payload digest binds to it. ' +
82
+ 'Fully OFFLINE — no network, no API key, no account; verifying someone else\'s receipt is the intended use. ' +
83
+ 'Use this for a receipt (money); use verify_compliance_record for an action record (a non-payment action). ' +
84
+ 'Returns { valid, signer, digest } and, when expectedSigner is supplied and does not match, valid:false with the recovered signer so you can see who actually signed.',
69
85
  inputSchema: {
70
- artifact: z.record(z.unknown()).describe('the receipt artifact object {format, payload, signature}'),
71
- expectedSigner: z.string().optional(),
86
+ artifact: z
87
+ .record(z.unknown())
88
+ .describe('the receipt artifact exactly as issued: { format, payload, signature }. Pass the object, not a JSON string'),
89
+ expectedSigner: z
90
+ .string()
91
+ .optional()
92
+ .describe('0x address the receipt MUST be signed by — obtain it out-of-band, never from the artifact. Omit to recover the signer without enforcing it'),
72
93
  },
73
94
  }, async ({ artifact, expectedSigner }) => json(await verifyReceiptTool(artifact, expectedSigner)));
74
95
  server.registerTool('record_disclosure', {
@@ -78,27 +99,49 @@ export function buildServer(deps) {
78
99
  text: z.string().optional().describe('the disclosure text as presented — digested locally, never transmitted'),
79
100
  textDigest: z.string().regex(/^0x[0-9a-fA-F]{64}$/).optional().describe('pre-computed digest (wins over text)'),
80
101
  medium: z.string().optional().describe("channel: 'chat' | 'api' | 'voice' | 'ui' …"),
81
- kind: z.enum(['ai-interaction', 'synthetic-content']).optional(),
82
- agentId: z.string().describe('stable identifier for the disclosing agent'),
83
- resourceUrl: z.string().url().optional(),
102
+ kind: z
103
+ .enum(['ai-interaction', 'synthetic-content'])
104
+ .optional()
105
+ .describe("what was disclosed: 'ai-interaction' = the user was told they are talking to an AI; 'synthetic-content' = output was marked machine-generated. Defaults to 'ai-interaction'"),
106
+ agentId: z.string().describe('stable identifier for the disclosing agent — keep it constant across calls so one chain accumulates per agent'),
107
+ resourceUrl: z.string().url().optional().describe('absolute URL of the surface the disclosure was presented on, when there is one'),
84
108
  },
85
109
  }, async (args) => json(await recordDisclosureTool(deps, args)));
86
110
  server.registerTool('verify_compliance_record', {
87
111
  title: 'Verify compliance record',
88
- description: 'Verify an Tersign compliance record + attestation (digest binding and signature).',
112
+ description: 'Verify a Tersign action record against its attestation: recompute the record\'s canonical digest, confirm the attestation commits to that exact digest, and recover the signature. ' +
113
+ 'Fully OFFLINE — no network, no API key, no account. ' +
114
+ 'Use this for an action record (a disclosure or other non-payment agent action); use verify_receipt for a payment receipt. ' +
115
+ 'PASS proves integrity and internal consistency only. Authorship needs an out-of-band signer address: pass expectedSigner, or the identity is whatever the artifact claims about itself. ' +
116
+ 'Returns { valid, signer, digest }; on mismatch, valid:false plus the recovered signer and the recomputed digest.',
89
117
  inputSchema: {
90
- record: z.record(z.unknown()),
91
- attestation: z.record(z.unknown()),
92
- expectedSigner: z.string().optional(),
118
+ record: z
119
+ .record(z.unknown())
120
+ .describe('the action record object as issued (ComplianceRecordV1 shape). Pass the object, not a JSON string; any field edit changes the digest and fails verification — which is the point'),
121
+ attestation: z
122
+ .record(z.unknown())
123
+ .describe('the attestation that accompanies the record: the signature over the record digest, as returned alongside it at issuance'),
124
+ expectedSigner: z
125
+ .string()
126
+ .optional()
127
+ .describe('0x address the record MUST be signed by, obtained out-of-band (for the public ledger: https://tersign.ai/v1/ledger). Omit to recover the signer without enforcing it'),
93
128
  },
94
129
  }, async ({ record, attestation, expectedSigner }) => json(await verifyRecordTool(record, attestation, expectedSigner)));
95
130
  server.registerTool('record_refund', {
96
131
  title: 'Record refund',
97
- description: 'Record a refund against a receipt digest in the Tersign ledger (requires ledger configuration).',
132
+ description: 'Record a refund against an already-chained receipt, as the SELLER. The refund becomes its own counter-signed entry that references the original nothing is edited or deleted, so the chain stays append-only and both the charge and the refund remain visible. ' +
133
+ 'Requires ledger configuration (TERSIGN_LEDGER_URL/_API_KEY/_SELLER_ID) and performs one network write; errors if the original digest is not on your chain. ' +
134
+ 'This RECORDS a refund you have already made — it moves no money. ' +
135
+ 'Returns the refund record, its digest, the ledger counter-signature and sequence number.',
98
136
  inputSchema: {
99
- originalDigest: z.string().regex(/^0x[0-9a-fA-F]{64}$/),
100
- amount: z.string(),
101
- reason: z.string(),
137
+ originalDigest: z
138
+ .string()
139
+ .regex(/^0x[0-9a-fA-F]{64}$/)
140
+ .describe('0x-prefixed keccak256 digest of the receipt being refunded — the digest returned by issue_receipt, and it must already exist on your chain'),
141
+ amount: z
142
+ .string()
143
+ .describe('refunded amount as a decimal STRING in the original settlement currency, e.g. "12.50". A string, not a number, so no precision is lost. Partial refunds are allowed'),
144
+ reason: z.string().describe('why the refund was issued, in your own words; recorded verbatim for whoever reads the chain later'),
102
145
  },
103
146
  }, async ({ originalDigest, amount, reason }) => json(await recordRefundTool(deps, originalDigest, amount, reason)));
104
147
  const digestSchema = z.string().regex(/^0x[0-9a-fA-F]{64}$/);
@@ -106,9 +149,13 @@ export function buildServer(deps) {
106
149
  title: 'Open dispute',
107
150
  description: 'Open an objective dispute against a counter-signed receipt as the PAYER (the configured key must be the receipt payer). Reasons: not_delivered, wrong_content, duplicate_charge. Contested non-mechanical claims escalate to the arbiter; duplicate_charge is decided instantly from ledger arithmetic.',
108
151
  inputSchema: {
109
- receiptDigest: digestSchema,
110
- reason: z.enum(['not_delivered', 'wrong_content', 'duplicate_charge']),
111
- claimAmount: z.string().describe('claimed refund in the settlement currency'),
152
+ receiptDigest: digestSchema.describe('0x-prefixed keccak256 digest of the counter-signed receipt being disputed'),
153
+ reason: z
154
+ .enum(['not_delivered', 'wrong_content', 'duplicate_charge'])
155
+ .describe("grounds: 'not_delivered' nothing arrived · 'wrong_content' delivered but not what was bought · 'duplicate_charge' the same supply was billed twice (decided mechanically from the chain, no arbiter)"),
156
+ claimAmount: z
157
+ .string()
158
+ .describe('amount claimed back, as a decimal STRING in the receipt\'s settlement currency, e.g. "12.50"; must not exceed the receipt amount'),
112
159
  statement: z.string().optional().describe('for humans reading the record — never an adjudication input'),
113
160
  },
114
161
  }, async ({ receiptDigest, reason, claimAmount, statement }) => json(await openDisputeTool(deps, { receiptDigest: receiptDigest, reason, claimAmount, statement })));
@@ -116,16 +163,21 @@ export function buildServer(deps) {
116
163
  title: 'Submit dispute evidence',
117
164
  description: 'Submit signed evidence to an open dispute. Claimant evidence must be signed by the payer key; respondent evidence additionally requires the seller API key (TERSIGN_LEDGER_API_KEY).',
118
165
  inputSchema: {
119
- disputeDigest: digestSchema,
120
- role: z.enum(['claimant', 'respondent']),
166
+ disputeDigest: digestSchema.describe('0x-prefixed digest of the open dispute, as returned by open_dispute'),
167
+ role: z
168
+ .enum(['claimant', 'respondent'])
169
+ .describe("which side you are filing as: 'claimant' = the payer who opened it (payer key) · 'respondent' = the seller answering it (also needs TERSIGN_LEDGER_API_KEY)"),
121
170
  artifacts: z
122
171
  .array(z.object({
123
- kind: z.enum(['content-digest', 'delivery-attestation', 'payment-proof', 'transcript']),
124
- digest: digestSchema,
125
- at: z.number().int().optional(),
126
- note: z.string().optional(),
172
+ kind: z
173
+ .enum(['content-digest', 'delivery-attestation', 'payment-proof', 'transcript'])
174
+ .describe('what this artifact is; the adjudicator treats each kind differently'),
175
+ digest: digestSchema.describe('0x-prefixed keccak256 digest of the artifact. Only the DIGEST is submitted — the content itself never leaves your side'),
176
+ at: z.number().int().optional().describe('unix seconds the artifact was produced; supply it when timing is part of your argument'),
177
+ note: z.string().optional().describe('short human-readable label for whoever reads the record; never an adjudication input'),
127
178
  }))
128
- .min(1),
179
+ .min(1)
180
+ .describe('at least one evidence reference; submit every artifact you want considered in a single call'),
129
181
  },
130
182
  }, async ({ disputeDigest, role, artifacts }) => json(await submitEvidenceTool(deps, {
131
183
  disputeDigest: disputeDigest,
@@ -134,13 +186,20 @@ export function buildServer(deps) {
134
186
  })));
135
187
  server.registerTool('adjudicate_dispute', {
136
188
  title: 'Adjudicate dispute',
137
- description: 'Trigger deterministic adjudication of an open dispute (public the v0 rulebook is recomputable by anyone). Refund verdicts create refund records automatically.',
138
- inputSchema: { disputeDigest: digestSchema },
189
+ description: 'Trigger deterministic adjudication of an open dispute. The v0 rulebook is public and the verdict is recomputable by anyone from the chain no discretion, no model in the loop. ' +
190
+ 'Side effects: writes a verdict entry, and a refund verdict automatically creates the corresponding refund record. Adjudicating twice is not meaningful; the first verdict stands. ' +
191
+ 'Returns the verdict, the rationale naming the rule applied, and the ledger signature over both.',
192
+ inputSchema: {
193
+ disputeDigest: digestSchema.describe('0x-prefixed digest of the open dispute to adjudicate, as returned by open_dispute'),
194
+ },
139
195
  }, async ({ disputeDigest }) => json(await adjudicateDisputeTool(deps, disputeDigest)));
140
196
  server.registerTool('get_dispute', {
141
197
  title: 'Get dispute record',
142
- description: 'Fetch a dispute record with its evidence, verdict, rationale, and ledger signature.',
143
- inputSchema: { disputeDigest: digestSchema },
198
+ description: 'Fetch a dispute in full: its state, both sides\' evidence references, the verdict and rationale once adjudicated, and the ledger signature over the record. ' +
199
+ 'Read-only one network read, no key required, and safe to poll while a dispute is open.',
200
+ inputSchema: {
201
+ disputeDigest: digestSchema.describe('0x-prefixed digest of the dispute to fetch, as returned by open_dispute'),
202
+ },
144
203
  }, async ({ disputeDigest }) => json(await getDisputeTool(deps, disputeDigest)));
145
204
  return server;
146
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tersign",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Tersign \u2014 the evidence layer for the agent economy. Counter-signed receipts, agent action records, idempotency enforcement, refunds, disputes, and jury-ready evidence envelopes for x402/agent-commerce sellers.",
5
5
  "license": "MIT",
6
6
  "type": "module",