@gera-services/mcp-gera-verify 1.0.0 → 1.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 CHANGED
@@ -39,6 +39,10 @@ with **no backend, no network, no auth**.
39
39
  | `get_trust_summary` | Aggregates the above into a short, **citation-ready sentence** (plus structured signals) for an agent to quote a grounded "what we can verify about X" answer. |
40
40
  | `issue_attestation` | **Gera Vouch.** Runs the same real verification and returns a **cryptographically signed (Ed25519) attestation receipt** an agent can present to a counterparty (who verifies it with the public key). Verdict is `pass` (a strong, source-attributed verifying signal exists) or `unverified` (not in our records — never a guess). A signed *proof-of-diligence receipt*; indemnity/underwriting is roadmap and is never implied. |
41
41
  | `get_vouch_public_key` | Returns the Ed25519 public key + `key_id` + verification recipe so **any party can independently verify** an `issue_attestation` receipt without trusting the transport. |
42
+ | `issue_mandate` | **Gera Agent Mandate.** A human/business grants an agent a scoped, revocable, **Ed25519-signed spend mandate** (cap, currency, merchant allowlist, categories, expiry). Pure authorization — no money moves. |
43
+ | `verify_mandate` | Verify a mandate before honouring an action: checks signature, expiry, spend cap, and merchant/category scope against the intended action. **Fails closed** — forged/expired/over-cap/out-of-scope all return `valid:false` with reasons. |
44
+ | `issue_receipt` | **Gera Ledger.** After an agent acts, mint a **signed receipt** of what happened — optionally linking the Vouch attestation + Agent Mandate it acted under. A verifiable proof-of-action (not a settlement). |
45
+ | `verify_receipt` | Verify a receipt's Ed25519 signature against the Gera issuer key — confirms Gera recorded the action, unaltered. Fails closed. |
42
46
 
43
47
  A typical agent flow: `check_business_trust` (or `get_trust_summary`) → drill
44
48
  into `lookup_food_hygiene` / `lookup_care_rating` / `verify_provider` for the
@@ -0,0 +1,33 @@
1
+ export interface Mandate {
2
+ mandate_version: string;
3
+ grantor: string;
4
+ agent_id: string;
5
+ max_amount: number | null;
6
+ currency: string;
7
+ merchant_allowlist: string[];
8
+ categories: string[];
9
+ purpose: string | null;
10
+ issued_at: string;
11
+ expires_at: string;
12
+ }
13
+ export interface MandateCheck {
14
+ amount?: number;
15
+ merchant?: string;
16
+ category?: string;
17
+ at?: string;
18
+ }
19
+ /**
20
+ * Verify a signed mandate against an optional intended action. Returns each
21
+ * check plus an overall `valid`, with human-readable reasons. Honest: an invalid
22
+ * signature, expiry, over-cap amount, or out-of-scope merchant/category all
23
+ * fail closed — never assumed-OK.
24
+ */
25
+ export declare function checkMandate(mandate: Mandate, signature_b64url: string, check?: MandateCheck): {
26
+ valid: boolean;
27
+ signature_valid: boolean;
28
+ not_expired: boolean;
29
+ within_cap: boolean;
30
+ merchant_in_scope: boolean;
31
+ category_in_scope: boolean;
32
+ reasons: string[];
33
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Gera Agent Mandate — scoped, revocable, signed spend authorizations.
3
+ *
4
+ * The permission boundary an AI agent must cross to act on a human's behalf: a
5
+ * human (or business) issues a signed mandate ("agent X may spend up to £200 at
6
+ * category Y until date Z"), and any executing party verifies the signature +
7
+ * scope before honouring an action. Pure authorization — no money moves here;
8
+ * this is the cryptographic permission layer.
9
+ *
10
+ * Reuses the Ed25519 signer in sign.ts (same issuer key as Gera Vouch).
11
+ */
12
+ import { verifyAttestation } from './sign.js';
13
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
14
+ /**
15
+ * Verify a signed mandate against an optional intended action. Returns each
16
+ * check plus an overall `valid`, with human-readable reasons. Honest: an invalid
17
+ * signature, expiry, over-cap amount, or out-of-scope merchant/category all
18
+ * fail closed — never assumed-OK.
19
+ */
20
+ export function checkMandate(mandate, signature_b64url, check = {}) {
21
+ const reasons = [];
22
+ const signature_valid = verifyAttestation(mandate, signature_b64url);
23
+ if (!signature_valid)
24
+ reasons.push('Signature does not verify against the Gera issuer key — the mandate is forged or altered.');
25
+ const now = check.at ? Date.parse(check.at) : Date.now();
26
+ const exp = Date.parse(mandate.expires_at);
27
+ const not_expired = Number.isFinite(exp) ? now <= exp : false;
28
+ if (!not_expired)
29
+ reasons.push(`Mandate expired (expires_at ${mandate.expires_at}).`);
30
+ let within_cap = true;
31
+ if (check.amount != null) {
32
+ within_cap = mandate.max_amount == null ? true : check.amount <= mandate.max_amount;
33
+ if (!within_cap)
34
+ reasons.push(`Amount ${check.amount} exceeds mandate cap ${mandate.max_amount} ${mandate.currency}.`);
35
+ }
36
+ let merchant_in_scope = true;
37
+ if (check.merchant != null && mandate.merchant_allowlist.length > 0) {
38
+ const want = norm(check.merchant);
39
+ merchant_in_scope = mandate.merchant_allowlist.some((m) => norm(m) === want);
40
+ if (!merchant_in_scope)
41
+ reasons.push(`Merchant "${check.merchant}" is not in the mandate allowlist.`);
42
+ }
43
+ let category_in_scope = true;
44
+ if (check.category != null && mandate.categories.length > 0) {
45
+ const want = norm(check.category);
46
+ category_in_scope = mandate.categories.some((c) => norm(c) === want);
47
+ if (!category_in_scope)
48
+ reasons.push(`Category "${check.category}" is not permitted by the mandate.`);
49
+ }
50
+ const valid = signature_valid && not_expired && within_cap && merchant_in_scope && category_in_scope;
51
+ if (valid)
52
+ reasons.push('Mandate is valid: signature verifies, not expired, and the action is within scope.');
53
+ return { valid, signature_valid, not_expired, within_cap, merchant_in_scope, category_in_scope, reasons };
54
+ }
package/dist/server.js CHANGED
@@ -23,7 +23,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
23
23
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
24
24
  import { z } from 'zod';
25
25
  import { fhrs, cqc, doctors, providers, findByName, norm, } from './data.js';
26
- import { signAttestation, publicKeyInfo } from './sign.js';
26
+ import { signAttestation, verifyAttestation, publicKeyInfo } from './sign.js';
27
+ import { checkMandate } from './mandate.js';
27
28
  function asText(payload) {
28
29
  return {
29
30
  content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
@@ -408,6 +409,128 @@ export function registerTools(server) {
408
409
  description: 'Returns the Ed25519 public key (and key_id + verification recipe) used to sign Gera Vouch attestations, so any party can independently verify an issue_attestation receipt without trusting the transport.',
409
410
  inputSchema: {},
410
411
  }, async () => asText(publicKeyInfo()));
412
+ // ── Tool 8: issue_mandate (Gera Agent Mandate) ────────────────────────────
413
+ // The permission boundary an agent crosses to act on a human's behalf: issues
414
+ // a scoped, time-boxed, Ed25519-signed mandate token. No money moves — this is
415
+ // pure authorization the executing party verifies before honouring an action.
416
+ server.registerTool('issue_mandate', {
417
+ title: 'Gera Agent Mandate: issue a scoped, signed spend mandate',
418
+ description: 'A human or business grants an AI agent scoped, revocable authority ("agent X may spend up to N at category/merchant Y until date Z"). Returns a cryptographically signed mandate any executing party verifies with verify_mandate before honouring an action. Pure authorization — Gera moves no money here.',
419
+ inputSchema: {
420
+ grantor: z.string().describe('Who is granting authority (person or business).'),
421
+ agent_id: z.string().describe('Identifier of the agent the mandate is granted to.'),
422
+ max_amount: z.number().optional().describe('Spend cap per action in the given currency. Omit for no cap.'),
423
+ currency: z.string().optional().describe('ISO currency code, e.g. "GBP". Defaults to GBP.'),
424
+ merchant_allowlist: z.array(z.string()).optional().describe('Only these merchants are permitted. Omit for any.'),
425
+ categories: z.array(z.string()).optional().describe('Only these spend categories are permitted. Omit for any.'),
426
+ purpose: z.string().optional().describe('Human-readable purpose recorded in the mandate.'),
427
+ expires_at: z.string().optional().describe('ISO 8601 expiry. Defaults to 30 days from now.'),
428
+ },
429
+ }, async ({ grantor, agent_id, max_amount, currency, merchant_allowlist, categories, purpose, expires_at }) => {
430
+ const now = new Date();
431
+ const exp = expires_at ?? new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString();
432
+ const mandate = {
433
+ mandate_version: '0.1',
434
+ grantor,
435
+ agent_id,
436
+ max_amount: max_amount ?? null,
437
+ currency: currency ?? 'GBP',
438
+ merchant_allowlist: merchant_allowlist ?? [],
439
+ categories: categories ?? [],
440
+ purpose: purpose ?? null,
441
+ issued_at: now.toISOString(),
442
+ expires_at: exp,
443
+ };
444
+ const signature = signAttestation(mandate);
445
+ return asText({
446
+ mandate,
447
+ signature,
448
+ verify: 'Pass `mandate` + `signature.signature_b64url` (plus the intended amount/merchant/category) to verify_mandate. Public key from get_vouch_public_key.',
449
+ note: 'A signed mandate is a revocable authorization, not a payment. Revoke by maintaining a revocation list keyed on issued_at + agent_id (roadmap: hosted revocation registry).',
450
+ });
451
+ });
452
+ // ── Tool 9: verify_mandate ────────────────────────────────────────────────
453
+ server.registerTool('verify_mandate', {
454
+ title: 'Gera Agent Mandate: verify a mandate before acting',
455
+ description: 'Before honouring an agent action, verify its mandate: checks the Ed25519 signature, expiry, spend cap, and merchant/category scope against the intended action. Fails closed — a forged, expired, over-cap, or out-of-scope mandate returns valid:false with reasons. Never assumes OK.',
456
+ inputSchema: {
457
+ mandate: z.record(z.any()).describe('The mandate object returned by issue_mandate.'),
458
+ signature_b64url: z.string().describe('The signature.signature_b64url from issue_mandate.'),
459
+ amount: z.number().optional().describe('Intended spend amount to check against the cap.'),
460
+ merchant: z.string().optional().describe('Intended merchant to check against the allowlist.'),
461
+ category: z.string().optional().describe('Intended category to check against the permitted set.'),
462
+ },
463
+ }, async ({ mandate, signature_b64url, amount, merchant, category }) => {
464
+ const result = checkMandate(mandate, signature_b64url, { amount, merchant, category });
465
+ return asText({ ...result, honesty_note: HONESTY_NOTE });
466
+ });
467
+ // ── Tool 10: issue_receipt (Gera Ledger) ──────────────────────────────────
468
+ // Completes the spine chain (verify -> vouch -> mandate -> RECEIPT): once an
469
+ // agent takes an action, it mints a cryptographically signed receipt that the
470
+ // action happened, optionally linking the attestation/mandate it acted under.
471
+ // A verifiable proof-of-action; the hosted append-only registry + dispute
472
+ // resolution is roadmap (this is the stateless signed-receipt primitive).
473
+ server.registerTool('issue_receipt', {
474
+ title: 'Gera Ledger: issue a signed receipt for a completed agent action',
475
+ description: 'After an AI agent acts (books, pays, dispatches), mint a cryptographically signed receipt recording what happened, optionally referencing the Vouch attestation and/or Agent Mandate it acted under. Any party verifies it with verify_receipt + the public key from get_vouch_public_key. A verifiable proof-of-action — not a settlement; Gera moves no money here.',
476
+ inputSchema: {
477
+ action: z.string().describe('What the agent did, e.g. "booked a cleaner".'),
478
+ agent_id: z.string().describe('Identifier of the acting agent.'),
479
+ subject: z.string().describe('Who/what the action was taken on (merchant, provider, counterparty).'),
480
+ outcome: z.enum(['completed', 'failed', 'pending']).optional().describe('Action outcome. Defaults to completed.'),
481
+ amount: z.number().optional().describe('Amount involved, if any.'),
482
+ currency: z.string().optional().describe('ISO currency code. Defaults to GBP when amount is set.'),
483
+ mandate_signature: z.string().optional().describe('The mandate signature this action was authorised under (links the receipt to its mandate).'),
484
+ attestation_signature: z.string().optional().describe('The Vouch attestation signature relied on (links the receipt to its diligence).'),
485
+ evidence: z.array(z.string()).optional().describe('Evidence references (URLs, geo/photo/check-in IDs). Stored verbatim, not validated.'),
486
+ occurred_at: z.string().optional().describe('ISO 8601 time the action occurred. Defaults to now.'),
487
+ },
488
+ }, async ({ action, agent_id, subject, outcome, amount, currency, mandate_signature, attestation_signature, evidence, occurred_at }) => {
489
+ const now = new Date();
490
+ const receipt = {
491
+ receipt_version: '0.1',
492
+ issuer: 'Gera Ledger — a Gera Systems product (gera.services)',
493
+ action,
494
+ agent_id,
495
+ subject,
496
+ outcome: outcome ?? 'completed',
497
+ amount: amount ?? null,
498
+ currency: amount != null ? currency ?? 'GBP' : null,
499
+ references: {
500
+ mandate_signature: mandate_signature ?? null,
501
+ attestation_signature: attestation_signature ?? null,
502
+ },
503
+ evidence: evidence ?? [],
504
+ occurred_at: occurred_at ?? now.toISOString(),
505
+ issued_at: now.toISOString(),
506
+ };
507
+ const signature = signAttestation(receipt);
508
+ return asText({
509
+ receipt,
510
+ signature,
511
+ verify: 'Pass `receipt` + `signature.signature_b64url` to verify_receipt. Public key from get_vouch_public_key.',
512
+ note: 'A signed receipt proves Gera recorded this action as stated, as of issued_at. It is a proof-of-action, not a settlement or guarantee. A hosted, queryable append-only ledger + dispute resolution is on the roadmap.',
513
+ });
514
+ });
515
+ // ── Tool 11: verify_receipt ───────────────────────────────────────────────
516
+ server.registerTool('verify_receipt', {
517
+ title: 'Gera Ledger: verify a signed action receipt',
518
+ description: 'Verify a receipt from issue_receipt: checks the Ed25519 signature against the Gera issuer key. Returns signature_valid plus the receipt, so any party can confirm Gera recorded this action without trusting the transport. Fails closed on any alteration.',
519
+ inputSchema: {
520
+ receipt: z.record(z.any()).describe('The receipt object returned by issue_receipt.'),
521
+ signature_b64url: z.string().describe('The signature.signature_b64url from issue_receipt.'),
522
+ },
523
+ }, async ({ receipt, signature_b64url }) => {
524
+ const signature_valid = verifyAttestation(receipt, signature_b64url);
525
+ return asText({
526
+ signature_valid,
527
+ receipt,
528
+ result: signature_valid
529
+ ? 'Valid: Gera recorded this action as stated; the receipt has not been altered.'
530
+ : 'INVALID: signature does not verify — the receipt is forged or altered.',
531
+ honesty_note: HONESTY_NOTE,
532
+ });
533
+ });
411
534
  }
412
535
  /**
413
536
  * Construct a fully-configured Gera Verify McpServer (all tools registered).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gera-services/mcp-gera-verify",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Gera Verify MCP server — \"Proof-of-Real\": let AI agents check if a UK business / food establishment / care provider is real and how trustworthy it is, using real verified FSA food-hygiene, CQC care-registry and Gera verified-provider data. Offline, source-attributed, no auth. A Gera Systems product.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.geraservicesuk/mcp-gera-verify",
4
4
  "description": "Verify if a UK business, food establishment or care provider is real (FSA, CQC and Gera data), and issue a cryptographically signed attestation an AI agent can present before it acts (Gera Vouch).",
5
- "version": "1.0.0",
5
+ "version": "1.2.0",
6
6
  "repository": {
7
7
  "url": "https://github.com/geraservicesuk/globetura",
8
8
  "source": "github",
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "@gera-services/mcp-gera-verify",
16
- "version": "1.0.0",
16
+ "version": "1.2.0",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }