@gera-services/mcp-gera-verify 1.0.0 → 1.1.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 +2 -0
- package/dist/mandate.d.ts +33 -0
- package/dist/mandate.js +54 -0
- package/dist/server.js +56 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ 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. |
|
|
42
44
|
|
|
43
45
|
A typical agent flow: `check_business_trust` (or `get_trust_summary`) → drill
|
|
44
46
|
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
|
+
};
|
package/dist/mandate.js
ADDED
|
@@ -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
|
@@ -24,6 +24,7 @@ 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
26
|
import { signAttestation, 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,61 @@ 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
|
+
});
|
|
411
467
|
}
|
|
412
468
|
/**
|
|
413
469
|
* 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.
|
|
3
|
+
"version": "1.1.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.
|
|
5
|
+
"version": "1.1.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.
|
|
16
|
+
"version": "1.1.0",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|