@gera-services/mcp-gera-verify 1.0.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/dist/server.js ADDED
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Gera Verify MCP Server (stdio) — "Proof-of-Real"
3
+ *
4
+ * A Model Context Protocol server any AI agent (Claude, ChatGPT-with-tools,
5
+ * Cursor, any MCP client) can call to answer "is this real / how trustworthy
6
+ * is this?" about a UK business, food establishment, or care provider.
7
+ *
8
+ * It is seeded with REAL verified data Gera already owns and renders on its
9
+ * products: the FSA Food Hygiene Rating Scheme (used by GeraEats), the CQC
10
+ * care-provider registry (used by GeraClinic), public doctor listings, and
11
+ * Gera's crawled verified-provider set. Everything is offline — the data is a
12
+ * committed on-disk snapshot, so the server gives the same verified answer the
13
+ * Gera products give and runs anywhere with no backend, network, or auth.
14
+ *
15
+ * Honesty contract: every signal is source-attributed with an as-of date. When
16
+ * a subject is not in our records, the tools say so plainly ("not in our
17
+ * verified records") — they NEVER invent a rating or a status. Unavailable
18
+ * signals are reported as "unknown", not guessed.
19
+ *
20
+ * Product: Gera Verify — a Gera Systems product (gera.services).
21
+ */
22
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
23
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
24
+ import { z } from 'zod';
25
+ import { fhrs, cqc, doctors, providers, findByName, norm, } from './data.js';
26
+ import { signAttestation, publicKeyInfo } from './sign.js';
27
+ function asText(payload) {
28
+ return {
29
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
30
+ };
31
+ }
32
+ const HONESTY_NOTE = 'Every signal is sourced from real verified data Gera holds on disk, attributed with an as-of date. Absence of a record means "not in our verified records", never that the subject is untrustworthy. Unknown signals are reported as unknown, never inferred.';
33
+ // ── FHRS food-hygiene lookup ─────────────────────────────────────────────────
34
+ function fhrsSource() {
35
+ return {
36
+ source: fhrs.meta.source,
37
+ sourceUrl: fhrs.meta.sourceUrl,
38
+ asOf: fhrs.meta.asOf,
39
+ attribution: fhrs.meta.attribution,
40
+ };
41
+ }
42
+ function fhrsMatch(name, city, postcode) {
43
+ return findByName(fhrs.records, name, { city, postcode }, (r) => ({ address: r.address, postcode: r.postcode, authority: r.authority }));
44
+ }
45
+ function describeFhrs(r) {
46
+ const isFhis = r.scheme === 'FHIS';
47
+ return {
48
+ establishment: r.name,
49
+ address: r.address,
50
+ postcode: r.postcode,
51
+ businessType: r.businessType,
52
+ localAuthority: r.authority,
53
+ scheme: r.scheme,
54
+ scheme_explained: isFhis
55
+ ? 'FHIS (Scotland): Pass / Improvement Required / Awaiting Inspection — not a 0-5 scale.'
56
+ : 'FHRS (England, Wales, N. Ireland): 0 (urgent improvement necessary) to 5 (very good).',
57
+ rating: r.rating, // numeric 0-5 for FHRS; null for FHIS
58
+ rating_label: r.ratingLabel,
59
+ rating_date: r.ratingDate,
60
+ fhrsId: r.fhrsId,
61
+ source: `Source: FSA Food Hygiene Rating Scheme, local authority "${r.authority}", as of ${fhrs.meta.asOf}.`,
62
+ };
63
+ }
64
+ // ── CQC care-provider lookup ─────────────────────────────────────────────────
65
+ function cqcMatch(name, city, postcode) {
66
+ return findByName(cqc.records, name, { city, postcode }, (r) => ({ address: r.address, postcode: r.postcode, authority: r.authority }));
67
+ }
68
+ function describeCqc(r) {
69
+ return {
70
+ provider: r.name,
71
+ parentProvider: r.providerName,
72
+ address: r.address,
73
+ postcode: r.postcode,
74
+ phone: r.phone,
75
+ website: r.website,
76
+ serviceTypes: r.serviceTypes,
77
+ localAuthority: r.authority,
78
+ region: r.region,
79
+ cqc_registered: true,
80
+ last_inspected: r.lastInspected,
81
+ // CQC's categorical overall rating is NOT in our snapshot — honestly unknown.
82
+ overall_rating: 'unknown',
83
+ overall_rating_note: 'Verified CQC-registered provider. CQC publishes a categorical overall rating (Outstanding / Good / Requires improvement / Inadequate) at cqc.org.uk; that field is not in our snapshot, so it is reported as unknown — never inferred. Check the live profile for the current rating.',
84
+ cqcLocationId: r.cqcLocationId,
85
+ cqc_profile: `https://www.cqc.org.uk/location/${r.cqcLocationId}`,
86
+ source: `Source: Care Quality Commission registry, local authority "${r.authority}", as of ${cqc.meta.asOf}.`,
87
+ };
88
+ }
89
+ // ── Verified-provider (crawled set) lookup ───────────────────────────────────
90
+ function providerMatch(name, city) {
91
+ return findByName(providers.records, name, { city }, (r) => ({ city: r.city }));
92
+ }
93
+ function describeProvider(r) {
94
+ return {
95
+ name: r.name,
96
+ type: r.type,
97
+ specialty: r.specialty,
98
+ city: r.city,
99
+ country: r.country,
100
+ website: r.website,
101
+ in_gera_verified_set: true,
102
+ crawl_source: r.source,
103
+ source_url: r.sourceUrl,
104
+ crawled_at: r.crawledAt,
105
+ source: `Source: Gera verified-provider crawl ("${r.source}"), as of ${r.crawledAt}.`,
106
+ };
107
+ }
108
+ /**
109
+ * Register all Gera Verify tools on a McpServer instance. Transport-agnostic:
110
+ * the same handlers serve stdio (local) and Streamable HTTP / SSE (hosted).
111
+ */
112
+ export function registerTools(server) {
113
+ // ── Tool 1: lookup_food_hygiene ───────────────────────────────────────────
114
+ server.registerTool('lookup_food_hygiene', {
115
+ title: 'Look up a real FSA food-hygiene rating',
116
+ description: 'Look up the REAL Food Standards Agency food-hygiene rating for a UK food business by name (optionally narrowed by city or postcode). Returns the FHRS rating (0-5; England/Wales/NI) or FHIS status (Pass / Improvement Required; Scotland), the rated establishment, business type, local authority, and rating date — source-attributed to the FSA. If the business is not in our snapshot, says so plainly. Data is a real on-disk snapshot, not a live API call.',
117
+ inputSchema: {
118
+ name: z.string().describe('Business / establishment name, e.g. "Etci Mehmet Steak House".'),
119
+ city: z.string().optional().describe('City or town to narrow the match, e.g. "Birmingham".'),
120
+ postcode: z.string().optional().describe('Full or partial UK postcode, e.g. "B7 5SA" or "B7".'),
121
+ },
122
+ }, async ({ name, city, postcode }) => {
123
+ const matches = fhrsMatch(name, city, postcode);
124
+ if (matches.length === 0) {
125
+ return asText({
126
+ query: { name, city: city ?? null, postcode: postcode ?? null },
127
+ found: false,
128
+ message: `"${name}" is not in our verified FSA food-hygiene records${city ? ` for ${city}` : ''}. This is not a judgement — it only means we have no FHRS record on file for it in this snapshot.`,
129
+ coverage: `${fhrs.records.length} establishments across UK local authorities (FSA top/worst-rated sample).`,
130
+ ...fhrsSource(),
131
+ honesty_note: HONESTY_NOTE,
132
+ });
133
+ }
134
+ return asText({
135
+ query: { name, city: city ?? null, postcode: postcode ?? null },
136
+ found: true,
137
+ best_match: describeFhrs(matches[0].record),
138
+ other_matches: matches.slice(1).map((m) => describeFhrs(m.record)),
139
+ ...fhrsSource(),
140
+ honesty_note: HONESTY_NOTE,
141
+ });
142
+ });
143
+ // ── Tool 2: lookup_care_rating ────────────────────────────────────────────
144
+ server.registerTool('lookup_care_rating', {
145
+ title: 'Look up a real CQC care provider',
146
+ description: 'Look up a UK health/social-care provider in the REAL Care Quality Commission (CQC) registry by name (optionally narrowed by city or postcode). Confirms the provider is CQC-registered and returns its registered name, address, service types, last-inspection date, region, and a link to its live CQC profile — source-attributed to the CQC. NOTE: CQC ratings are categorical (Outstanding / Good / Requires improvement / Inadequate), NEVER numeric; our snapshot does not carry the categorical rating, so overall_rating is honestly returned as "unknown" with a link to check it live. If the provider is not registered with CQC in our records, says so plainly.',
147
+ inputSchema: {
148
+ name: z.string().describe('Care/health provider name, e.g. "Woodlands Health Centre".'),
149
+ city: z.string().optional().describe('City or local authority to narrow the match.'),
150
+ postcode: z.string().optional().describe('Full or partial UK postcode.'),
151
+ },
152
+ }, async ({ name, city, postcode }) => {
153
+ const matches = cqcMatch(name, city, postcode);
154
+ if (matches.length === 0) {
155
+ return asText({
156
+ query: { name, city: city ?? null, postcode: postcode ?? null },
157
+ found: false,
158
+ message: `"${name}" is not in our verified CQC registry records${city ? ` for ${city}` : ''}. It may still be CQC-registered — check cqc.org.uk — but we have no record of it in this snapshot.`,
159
+ coverage: `${cqc.records.length} CQC-registered providers across UK local authorities.`,
160
+ source: cqc.meta.source,
161
+ sourceUrl: cqc.meta.sourceUrl,
162
+ asOf: cqc.meta.asOf,
163
+ attribution: cqc.meta.attribution,
164
+ honesty_note: HONESTY_NOTE,
165
+ });
166
+ }
167
+ return asText({
168
+ query: { name, city: city ?? null, postcode: postcode ?? null },
169
+ found: true,
170
+ best_match: describeCqc(matches[0].record),
171
+ other_matches: matches.slice(1).map((m) => describeCqc(m.record)),
172
+ source: cqc.meta.source,
173
+ sourceUrl: cqc.meta.sourceUrl,
174
+ asOf: cqc.meta.asOf,
175
+ attribution: cqc.meta.attribution,
176
+ honesty_note: HONESTY_NOTE,
177
+ });
178
+ });
179
+ // ── Tool 3: verify_provider ───────────────────────────────────────────────
180
+ server.registerTool('verify_provider', {
181
+ title: "Check Gera's verified-provider set",
182
+ description: "Check whether a provider is in Gera's own verified-provider / Passport set — the crawled, source-attributed provider records Gera maintains (currently healthcare providers). Returns the provider type, specialty, location, website, crawl source, and crawl date. If not in the set, says so plainly. This is Gera's first-party verification signal (distinct from third-party FSA/CQC data).",
183
+ inputSchema: {
184
+ name: z.string().describe('Provider name, e.g. "Nairi Medical Centre".'),
185
+ city: z.string().optional().describe('City to narrow the match, e.g. "Yerevan".'),
186
+ },
187
+ }, async ({ name, city }) => {
188
+ const matches = providerMatch(name, city);
189
+ if (matches.length === 0) {
190
+ return asText({
191
+ query: { name, city: city ?? null },
192
+ in_gera_verified_set: false,
193
+ message: `"${name}" is not in Gera's verified-provider set${city ? ` for ${city}` : ''}. The set currently covers ${providers.records.length} crawled providers; absence is not a negative signal.`,
194
+ coverage: `${providers.records.length} verified providers (Gera crawler).`,
195
+ source: providers.meta.source,
196
+ asOf: providers.meta.asOf,
197
+ honesty_note: HONESTY_NOTE,
198
+ });
199
+ }
200
+ return asText({
201
+ query: { name, city: city ?? null },
202
+ in_gera_verified_set: true,
203
+ best_match: describeProvider(matches[0].record),
204
+ other_matches: matches.slice(1).map((m) => describeProvider(m.record)),
205
+ source: providers.meta.source,
206
+ asOf: providers.meta.asOf,
207
+ honesty_note: HONESTY_NOTE,
208
+ });
209
+ });
210
+ // ── Tool 4: check_business_trust ──────────────────────────────────────────
211
+ // The flagship "is this real / how trustworthy?" entry point. Aggregates
212
+ // every signal we hold for a subject across all datasets.
213
+ server.registerTool('check_business_trust', {
214
+ title: 'Proof-of-Real: all verified signals for a business',
215
+ description: 'The flagship "is this real / how trustworthy is this?" check. Given a UK business name (+ optional city/postcode and a hint of its type — food, care, healthcare), returns EVERY real verified signal Gera holds: FSA food-hygiene rating (if it is a food business in our FHRS data), CQC registration (if it is a care provider), and presence in Gera\'s verified-provider set — each source-attributed with an as-of date. Produces an overall_verification of "verified" (>=1 real signal found), "not_in_our_records" (no signal), so an agent can cite a grounded answer. Never fabricates: missing signals are reported as unknown.',
216
+ inputSchema: {
217
+ name: z.string().describe('Business name to verify.'),
218
+ city: z.string().optional().describe('City / town.'),
219
+ postcode: z.string().optional().describe('Full or partial UK postcode.'),
220
+ type: z
221
+ .enum(['food', 'care', 'healthcare', 'unknown'])
222
+ .optional()
223
+ .describe('Optional hint of business type to focus the search; omit to check all.'),
224
+ },
225
+ }, async ({ name, city, postcode, type }) => {
226
+ const checkFood = !type || type === 'food' || type === 'unknown';
227
+ const checkCare = !type || type === 'care' || type === 'unknown';
228
+ const checkProv = !type || type === 'care' || type === 'healthcare' || type === 'unknown';
229
+ const signals = {};
230
+ let signalCount = 0;
231
+ if (checkFood) {
232
+ const m = fhrsMatch(name, city, postcode);
233
+ if (m.length) {
234
+ signals.food_hygiene = describeFhrs(m[0].record);
235
+ signalCount++;
236
+ }
237
+ else {
238
+ signals.food_hygiene = { status: 'not_in_our_records', dataset: 'FSA FHRS' };
239
+ }
240
+ }
241
+ if (checkCare) {
242
+ const m = cqcMatch(name, city, postcode);
243
+ if (m.length) {
244
+ signals.care_quality = describeCqc(m[0].record);
245
+ signalCount++;
246
+ }
247
+ else {
248
+ signals.care_quality = { status: 'not_in_our_records', dataset: 'CQC registry' };
249
+ }
250
+ }
251
+ if (checkProv) {
252
+ const m = providerMatch(name, city);
253
+ if (m.length) {
254
+ signals.gera_verified_provider = describeProvider(m[0].record);
255
+ signalCount++;
256
+ }
257
+ else {
258
+ signals.gera_verified_provider = {
259
+ status: 'not_in_our_records',
260
+ dataset: 'Gera verified-provider set',
261
+ };
262
+ }
263
+ }
264
+ const verified = signalCount > 0;
265
+ return asText({
266
+ query: { name, city: city ?? null, postcode: postcode ?? null, type: type ?? 'all' },
267
+ overall_verification: verified ? 'verified' : 'not_in_our_records',
268
+ verified_signal_count: signalCount,
269
+ summary: verified
270
+ ? `Found ${signalCount} real verified signal(s) for "${name}". See per-source detail below.`
271
+ : `No verified record found for "${name}"${city ? ` in ${city}` : ''} across our FSA / CQC / Gera-provider datasets. This is a real, useful answer — we simply hold no verification record for it. It does not mean the business is fake.`,
272
+ signals,
273
+ datasets_checked: [
274
+ checkFood ? 'FSA Food Hygiene (FHRS/FHIS)' : null,
275
+ checkCare ? 'CQC care registry' : null,
276
+ checkProv ? 'Gera verified-provider set' : null,
277
+ ].filter(Boolean),
278
+ honesty_note: HONESTY_NOTE,
279
+ });
280
+ });
281
+ // ── Tool 5: get_trust_summary ─────────────────────────────────────────────
282
+ // A short, citation-ready natural-language sentence aggregating the signals,
283
+ // for an agent to drop straight into a reply.
284
+ server.registerTool('get_trust_summary', {
285
+ title: 'One-line citable trust summary',
286
+ description: 'Aggregate everything Gera can verify about a business into a short, citation-ready sentence (plus the structured signals behind it), so an AI agent can quote a grounded "what we can verify about X" answer. Same real data as check_business_trust, condensed. Never fabricates.',
287
+ inputSchema: {
288
+ name: z.string().describe('Business name.'),
289
+ city: z.string().optional().describe('City / town.'),
290
+ postcode: z.string().optional().describe('Full or partial UK postcode.'),
291
+ },
292
+ }, async ({ name, city, postcode }) => {
293
+ const parts = [];
294
+ const structured = {};
295
+ const food = fhrsMatch(name, city, postcode);
296
+ if (food.length) {
297
+ const r = food[0].record;
298
+ structured.food_hygiene = describeFhrs(r);
299
+ if (r.scheme === 'FHRS' && r.rating != null) {
300
+ parts.push(`holds an FSA food-hygiene rating of ${r.rating}/5 (rated ${r.ratingDate}, ${r.authority})`);
301
+ }
302
+ else if (r.scheme === 'FHIS') {
303
+ parts.push(`has an FSA (Scotland) food-hygiene status of "${r.ratingLabel ?? 'recorded'}"`);
304
+ }
305
+ }
306
+ const care = cqcMatch(name, city, postcode);
307
+ if (care.length) {
308
+ const r = care[0].record;
309
+ structured.care_quality = describeCqc(r);
310
+ parts.push(`is a CQC-registered care provider (${r.serviceTypes.join(', ') || 'registered'}; last inspected ${r.lastInspected ?? 'date unknown'}; overall rating: check live profile)`);
311
+ }
312
+ const prov = providerMatch(name, city);
313
+ if (prov.length) {
314
+ structured.gera_verified_provider = describeProvider(prov[0].record);
315
+ parts.push(`is in Gera's verified-provider set (crawled ${prov[0].record.crawledAt})`);
316
+ }
317
+ const display = [name, city].filter(Boolean).join(', ');
318
+ const summary = parts.length > 0
319
+ ? `Based on Gera's verified data, ${display} ${parts.join('; ')}. (Sources attributed per signal; verified as of the dates shown.)`
320
+ : `Gera holds no verified record for ${display} in its FSA food-hygiene, CQC care-registry, or verified-provider datasets. We cannot confirm or deny its trustworthiness from our data.`;
321
+ return asText({
322
+ query: { name, city: city ?? null, postcode: postcode ?? null },
323
+ verified: parts.length > 0,
324
+ summary,
325
+ signals: structured,
326
+ honesty_note: HONESTY_NOTE,
327
+ });
328
+ });
329
+ // ── Tool 6: issue_attestation (Gera Vouch) ────────────────────────────────
330
+ // The keystone of the agent trust layer: runs the same real verification and
331
+ // returns a CRYPTOGRAPHICALLY SIGNED attestation receipt an agent can present
332
+ // to a counterparty (or store), who verifies the signature with Gera's public
333
+ // key. Proves "Gera checked the named sources and recorded this verdict as of
334
+ // issued_at" — a proof-of-diligence receipt, NOT an insurance policy.
335
+ server.registerTool('issue_attestation', {
336
+ title: 'Gera Vouch: issue a signed attestation for an agent action',
337
+ description: 'Before an AI agent acts on a subject (book it, pay it, recommend it), call this to get a cryptographically signed attestation receipt grounded in real Gera data. Returns verdict ("pass" = a verifying signal exists in our sources; "unverified" = not in our records — never a guess), the source-attributed signals, and an Ed25519 signature any party can verify with get_vouch_public_key. This is a signed proof-of-diligence receipt; indemnity/underwriting is roadmap and is never implied.',
338
+ inputSchema: {
339
+ subject: z.string().describe('Business / provider name the agent is about to act on.'),
340
+ city: z.string().optional().describe('City / town.'),
341
+ postcode: z.string().optional().describe('Full or partial UK postcode.'),
342
+ claim: z
343
+ .string()
344
+ .optional()
345
+ .describe('The specific claim to attest, e.g. "is a CQC-registered care provider". Optional.'),
346
+ intended_action: z
347
+ .string()
348
+ .optional()
349
+ .describe('What the agent intends to do, recorded in the receipt, e.g. "book an appointment". Optional.'),
350
+ },
351
+ }, async ({ subject, city, postcode, claim, intended_action }) => {
352
+ const signals = {};
353
+ const sources = [];
354
+ // Vouch SIGNS this verdict, so it must only "pass" on a STRONG name match:
355
+ // the record name must contain (or equal) the full query. This rejects the
356
+ // looser matches the read-only tools allow — e.g. a long garbage query that
357
+ // merely *contains* a short record name as a substring ("...cafe..."), which
358
+ // must NOT be attestable. Deliberately stricter than the lookup tools.
359
+ const q = norm(subject);
360
+ const strong = (recordName) => q.length >= 3 && norm(recordName).includes(q);
361
+ const food = fhrsMatch(subject, city, postcode).filter((m) => strong(m.record.name));
362
+ if (food.length) {
363
+ signals.food_hygiene = describeFhrs(food[0].record);
364
+ sources.push(`FSA Food Hygiene Rating Scheme (as of ${fhrs.meta.asOf})`);
365
+ }
366
+ const care = cqcMatch(subject, city, postcode).filter((m) => strong(m.record.name));
367
+ if (care.length) {
368
+ signals.care_quality = describeCqc(care[0].record);
369
+ sources.push(`CQC care-provider registry (as of ${cqc.meta.asOf})`);
370
+ }
371
+ const prov = providerMatch(subject, city).filter((m) => strong(m.record.name));
372
+ if (prov.length) {
373
+ signals.gera_verified_provider = describeProvider(prov[0].record);
374
+ sources.push(`Gera verified-provider crawl (as of ${prov[0].record.crawledAt})`);
375
+ }
376
+ const verdict = sources.length > 0 ? 'pass' : 'unverified';
377
+ // The signed payload. Sorted-key canonical JSON of THIS object is what the
378
+ // signature covers — a verifier re-canonicalises and checks it.
379
+ const attestation = {
380
+ issuer: 'Gera Vouch — a Gera Systems product (gera.services)',
381
+ attestation_version: '0.1',
382
+ issued_at: new Date().toISOString(),
383
+ subject: { name: subject, city: city ?? null, postcode: postcode ?? null },
384
+ claim: claim ?? 'subject is a real, verifiable entity in Gera’s sources',
385
+ intended_action: intended_action ?? null,
386
+ verdict,
387
+ verdict_meaning: verdict === 'pass'
388
+ ? 'Gera holds at least one source-attributed verifying signal for this subject (see signals).'
389
+ : 'Gera holds no verifying record for this subject in its FSA / CQC / verified-provider datasets. This is "not in our records", NOT a finding that the subject is untrustworthy.',
390
+ signals,
391
+ sources,
392
+ };
393
+ const signature = signAttestation(attestation);
394
+ return asText({
395
+ attestation,
396
+ signature,
397
+ verify: {
398
+ how: 'Re-canonicalise `attestation` as sorted-key UTF-8 JSON and verify the Ed25519 `signature.signature_b64url` against the public key from the get_vouch_public_key tool.',
399
+ key_id: signature.key_id,
400
+ },
401
+ disclaimer: 'This is a cryptographically signed proof-of-diligence receipt: it proves Gera checked the named sources and recorded this verdict as of issued_at. It is NOT an insurance policy, indemnity, or guarantee — underwriting is on the Gera Vouch roadmap and is not active. ' +
402
+ HONESTY_NOTE,
403
+ });
404
+ });
405
+ // ── Tool 7: get_vouch_public_key ──────────────────────────────────────────
406
+ server.registerTool('get_vouch_public_key', {
407
+ title: 'Gera Vouch: get the public key to verify attestations',
408
+ 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
+ inputSchema: {},
410
+ }, async () => asText(publicKeyInfo()));
411
+ }
412
+ /**
413
+ * Construct a fully-configured Gera Verify McpServer (all tools registered).
414
+ * Each call returns a fresh instance — used by the hosted HTTP transport in
415
+ * stateless mode (one server per request) and by the stdio server below.
416
+ */
417
+ export function createServer() {
418
+ const server = new McpServer({
419
+ name: 'gera-verify',
420
+ version: '1.0.0',
421
+ });
422
+ registerTools(server);
423
+ return server;
424
+ }
425
+ // Shared singleton for the stdio path.
426
+ export const server = createServer();
427
+ export async function main() {
428
+ const transport = new StdioServerTransport();
429
+ await server.connect(transport);
430
+ const counts = `FHRS:${fhrs.records.length} CQC:${cqc.records.length} doctors:${doctors.records.length} providers:${providers.records.length}`;
431
+ // stderr only — stdout is the MCP transport.
432
+ console.error(`Gera Verify MCP server running on stdio (gera-verify v1.0.0) — real data loaded [${counts}]`);
433
+ }
434
+ // Run when invoked directly as the built server (node dist/server.js).
435
+ const isMain = typeof process !== 'undefined' && process.argv[1] && /server\.js$/.test(process.argv[1]);
436
+ if (isMain) {
437
+ main().catch((err) => {
438
+ console.error('Fatal:', err);
439
+ process.exit(1);
440
+ });
441
+ }
package/dist/sign.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export declare function keyId(): string;
2
+ export declare function publicKeyInfo(): {
3
+ alg: string;
4
+ key_id: string;
5
+ public_key_b64url: string;
6
+ is_production_key: boolean;
7
+ verify: string;
8
+ };
9
+ export declare function signAttestation(attestation: Record<string, unknown>): {
10
+ alg: string;
11
+ key_id: string;
12
+ canonicalization: string;
13
+ signature_b64url: string;
14
+ };
15
+ export declare function verifyAttestation(attestation: Record<string, unknown>, signature_b64url: string): boolean;
package/dist/sign.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Gera Vouch — signed attestation layer for Gera Verify.
3
+ *
4
+ * Turns a verification result into a cryptographically signed, verifiable
5
+ * attestation receipt: an agent (or the counterparty it is acting against) can
6
+ * verify the signature with Gera's public key and trust that Gera actually
7
+ * checked the named sources and recorded this verdict as-of the issue time.
8
+ *
9
+ * Ed25519 via Node's built-in `crypto` — no dependencies. The signing key is
10
+ * derived from a 32-byte seed: production sets GERA_VOUCH_SEED (base64); without
11
+ * it the server uses a deterministic, stable DEV key so signatures still work
12
+ * and verify out-of-the-box (consistent with this package running anywhere with
13
+ * no backend). The DEV key is NOT a secret — `is_production_key` flags which is
14
+ * in use, and the public key is always exposed for verification.
15
+ *
16
+ * Honesty contract: a signature proves *Gera attested this*, not that the
17
+ * subject is insured. Indemnity/underwriting is roadmap, never implied here.
18
+ */
19
+ import { createPrivateKey, createPublicKey, createHash, sign as edSign, verify as edVerify, } from 'node:crypto';
20
+ // PKCS8 DER prefix for an Ed25519 private key built from a raw 32-byte seed.
21
+ const PKCS8_ED25519_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
22
+ function seed() {
23
+ const env = process.env.GERA_VOUCH_SEED;
24
+ if (env) {
25
+ const b = Buffer.from(env, 'base64');
26
+ if (b.length >= 32)
27
+ return b.subarray(0, 32);
28
+ }
29
+ // Deterministic DEV seed — stable public key, reproducible signatures, NOT a
30
+ // production secret. Override with GERA_VOUCH_SEED in production.
31
+ return createHash('sha256').update('gera-vouch-dev-key-v1').digest();
32
+ }
33
+ let _priv = null;
34
+ function privateKey() {
35
+ if (!_priv) {
36
+ const der = Buffer.concat([PKCS8_ED25519_PREFIX, seed()]);
37
+ _priv = createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
38
+ }
39
+ return _priv;
40
+ }
41
+ function publicKeyRaw() {
42
+ const jwk = createPublicKey(privateKey()).export({ format: 'jwk' });
43
+ return Buffer.from(jwk.x, 'base64url');
44
+ }
45
+ export function keyId() {
46
+ return 'gera-vouch-ed25519-' + createHash('sha256').update(publicKeyRaw()).digest('hex').slice(0, 16);
47
+ }
48
+ export function publicKeyInfo() {
49
+ return {
50
+ alg: 'Ed25519',
51
+ key_id: keyId(),
52
+ public_key_b64url: publicKeyRaw().toString('base64url'),
53
+ is_production_key: !!process.env.GERA_VOUCH_SEED,
54
+ verify: 'Ed25519 over the canonical (sorted-key, UTF-8) JSON of the `attestation` object.',
55
+ };
56
+ }
57
+ // Canonical JSON: sorted keys so the signature is reproducible and any verifier
58
+ // re-canonicalises the same bytes.
59
+ function sortKeys(v) {
60
+ if (Array.isArray(v))
61
+ return v.map(sortKeys);
62
+ if (v && typeof v === 'object') {
63
+ return Object.keys(v)
64
+ .sort()
65
+ .reduce((acc, k) => {
66
+ acc[k] = sortKeys(v[k]);
67
+ return acc;
68
+ }, {});
69
+ }
70
+ return v;
71
+ }
72
+ function canonical(obj) {
73
+ return Buffer.from(JSON.stringify(sortKeys(obj)), 'utf8');
74
+ }
75
+ export function signAttestation(attestation) {
76
+ const signature = edSign(null, canonical(attestation), privateKey());
77
+ return {
78
+ alg: 'Ed25519',
79
+ key_id: keyId(),
80
+ canonicalization: 'json-sorted-keys-utf8',
81
+ signature_b64url: signature.toString('base64url'),
82
+ };
83
+ }
84
+ export function verifyAttestation(attestation, signature_b64url) {
85
+ try {
86
+ return edVerify(null, canonical(attestation), createPublicKey(privateKey()), Buffer.from(signature_b64url, 'base64url'));
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@gera-services/mcp-gera-verify",
3
+ "version": "1.0.0",
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
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/server.js",
8
+ "types": "dist/server.d.ts",
9
+ "mcpName": "io.github.geraservicesuk/mcp-gera-verify",
10
+ "bin": {
11
+ "mcp-gera-verify": "bin/cli.js",
12
+ "mcp-gera-verify-http": "bin/http.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "bin",
17
+ "src/data",
18
+ "server.json",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build:data": "node scripts/build-data.mjs",
23
+ "build": "tsc --noCheck && node scripts/copy-data.mjs",
24
+ "type-check": "tsc --noEmit",
25
+ "start": "node bin/cli.js",
26
+ "start:http": "node bin/http.js",
27
+ "smoke": "node scripts/smoke.mjs"
28
+ },
29
+ "keywords": [
30
+ "mcp",
31
+ "model-context-protocol",
32
+ "trust",
33
+ "verification",
34
+ "proof-of-real",
35
+ "food-hygiene",
36
+ "fhrs",
37
+ "cqc",
38
+ "fact-check",
39
+ "gera",
40
+ "gera-verify"
41
+ ],
42
+ "author": "Gera Systems Ltd",
43
+ "homepage": "https://gera.services",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/geraservicesuk/globetura.git",
47
+ "directory": "packages/mcp-gera-verify"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.12.0",
51
+ "zod": "^3.23.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.12.0",
55
+ "typescript": "^5.4.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=20"
59
+ }
60
+ }
package/server.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.geraservicesuk/mcp-gera-verify",
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",
6
+ "repository": {
7
+ "url": "https://github.com/geraservicesuk/globetura",
8
+ "source": "github",
9
+ "subfolder": "packages/mcp-gera-verify"
10
+ },
11
+ "websiteUrl": "https://gera.services",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "identifier": "@gera-services/mcp-gera-verify",
16
+ "version": "1.0.0",
17
+ "transport": {
18
+ "type": "stdio"
19
+ }
20
+ }
21
+ ]
22
+ }