@gera-services/mcp-gera-nexus 0.1.1 → 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/README.md +63 -14
- package/bin/cli.js +11 -1
- package/data/gerahome_providers_yerevan.csv +111 -0
- package/data/gerajobs_providers.csv +21 -0
- package/dist/data.d.ts +92 -0
- package/dist/data.js +307 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/intents.d.ts +50 -0
- package/dist/intents.js +94 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +318 -0
- package/package.json +22 -18
- package/server.json +7 -5
package/dist/data.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data layer for the GeraNexus action-rails MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Three honest data strategies, in priority order, per marketplace:
|
|
5
|
+
*
|
|
6
|
+
* 1. LIVE Gera marketplace API (best-effort) — if the production backend is
|
|
7
|
+
* reachable and returns rows, we use them and label the source "live_api".
|
|
8
|
+
* 2. LIVE public open data — for restaurants we call the UK Food Standards
|
|
9
|
+
* Agency FHRS open API (api.ratings.food.gov.uk, no auth), which is
|
|
10
|
+
* genuinely live UK restaurant data. Labelled "live_fhrs".
|
|
11
|
+
* 3. Bundled real crawled seed data — the CSVs shipped in ./data were
|
|
12
|
+
* crawled from List.am/Spyur (Yerevan home-service providers) and from
|
|
13
|
+
* real UK recruitment agencies. Labelled "bundled_seed".
|
|
14
|
+
*
|
|
15
|
+
* Every result carries a `data_source` field so an agent can tell a user
|
|
16
|
+
* exactly where the data came from. We NEVER fabricate rows.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
// dist/ sits next to data/ at the package root: dist/data.js -> ../data
|
|
23
|
+
const DATA_DIR = join(__dirname, '..', 'data');
|
|
24
|
+
// Production base URLs for the live Gera marketplace backends (Railway).
|
|
25
|
+
// These are real, but their databases may be empty/seeded, so every call is
|
|
26
|
+
// best-effort with a short timeout and a graceful fall-back to seed data.
|
|
27
|
+
export const MARKETPLACE_API = {
|
|
28
|
+
home: 'https://taskova-production-ace3.up.railway.app',
|
|
29
|
+
jobs: 'https://gerajobs-production.up.railway.app',
|
|
30
|
+
eats: 'https://fooddash-production-0c1e.up.railway.app',
|
|
31
|
+
};
|
|
32
|
+
const PUBLIC_SURFACE = {
|
|
33
|
+
home: 'https://gerahome.com',
|
|
34
|
+
jobs: 'https://gerajobs.com',
|
|
35
|
+
eats: 'https://geraeats.com',
|
|
36
|
+
};
|
|
37
|
+
// ─── Minimal CSV parser (handles quoted fields + embedded commas) ─────────────
|
|
38
|
+
function parseCsv(text) {
|
|
39
|
+
const rows = [];
|
|
40
|
+
let field = '';
|
|
41
|
+
let row = [];
|
|
42
|
+
let inQuotes = false;
|
|
43
|
+
for (let i = 0; i < text.length; i++) {
|
|
44
|
+
const c = text[i];
|
|
45
|
+
if (inQuotes) {
|
|
46
|
+
if (c === '"') {
|
|
47
|
+
if (text[i + 1] === '"') {
|
|
48
|
+
field += '"';
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
inQuotes = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
field += c;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (c === '"') {
|
|
60
|
+
inQuotes = true;
|
|
61
|
+
}
|
|
62
|
+
else if (c === ',') {
|
|
63
|
+
row.push(field);
|
|
64
|
+
field = '';
|
|
65
|
+
}
|
|
66
|
+
else if (c === '\n' || c === '\r') {
|
|
67
|
+
if (c === '\r' && text[i + 1] === '\n')
|
|
68
|
+
i++;
|
|
69
|
+
if (field !== '' || row.length > 0) {
|
|
70
|
+
row.push(field);
|
|
71
|
+
rows.push(row);
|
|
72
|
+
row = [];
|
|
73
|
+
field = '';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
field += c;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (field !== '' || row.length > 0) {
|
|
81
|
+
row.push(field);
|
|
82
|
+
rows.push(row);
|
|
83
|
+
}
|
|
84
|
+
if (rows.length === 0)
|
|
85
|
+
return [];
|
|
86
|
+
const header = rows[0];
|
|
87
|
+
return rows.slice(1).map((r) => {
|
|
88
|
+
const obj = {};
|
|
89
|
+
header.forEach((h, idx) => {
|
|
90
|
+
obj[h.trim()] = (r[idx] ?? '').trim();
|
|
91
|
+
});
|
|
92
|
+
return obj;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
let _homeSeed = null;
|
|
96
|
+
let _jobsSeed = null;
|
|
97
|
+
function homeSeed() {
|
|
98
|
+
if (_homeSeed === null) {
|
|
99
|
+
try {
|
|
100
|
+
_homeSeed = parseCsv(readFileSync(join(DATA_DIR, 'gerahome_providers_yerevan.csv'), 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
_homeSeed = [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return _homeSeed;
|
|
107
|
+
}
|
|
108
|
+
function jobsSeed() {
|
|
109
|
+
if (_jobsSeed === null) {
|
|
110
|
+
try {
|
|
111
|
+
_jobsSeed = parseCsv(readFileSync(join(DATA_DIR, 'gerajobs_providers.csv'), 'utf8'));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
_jobsSeed = [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return _jobsSeed;
|
|
118
|
+
}
|
|
119
|
+
// ─── best-effort live fetch with timeout ──────────────────────────────────────
|
|
120
|
+
async function fetchJson(url, opts = {}) {
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 4000);
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(url, {
|
|
125
|
+
headers: opts.headers,
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok)
|
|
129
|
+
return null;
|
|
130
|
+
return await res.json();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ─── HOME SERVICES ────────────────────────────────────────────────────────────
|
|
140
|
+
export async function searchHomeServices(args) {
|
|
141
|
+
// 1. Best-effort live GeraHome API.
|
|
142
|
+
const q = new URLSearchParams();
|
|
143
|
+
if (args.city)
|
|
144
|
+
q.set('city', args.city);
|
|
145
|
+
q.set('limit', String(args.limit));
|
|
146
|
+
const live = (await fetchJson(`${MARKETPLACE_API.home}/api/v1/providers?${q.toString()}`, { headers: { 'x-tenant-id': 'AM' } }));
|
|
147
|
+
const liveRows = (live?.data ?? live?.items ?? null);
|
|
148
|
+
if (Array.isArray(liveRows) && liveRows.length > 0) {
|
|
149
|
+
const providers = liveRows.slice(0, args.limit).map((r, i) => ({
|
|
150
|
+
id: String(r.id ?? `home-live-${i}`),
|
|
151
|
+
name: String(r.business_name ?? r.name ?? 'Provider'),
|
|
152
|
+
category: String(r.category ?? r.category_name ?? args.category ?? 'general'),
|
|
153
|
+
city: String(r.city ?? args.city ?? ''),
|
|
154
|
+
area: r.district ? String(r.district) : undefined,
|
|
155
|
+
phone: r.phone ? String(r.phone) : undefined,
|
|
156
|
+
}));
|
|
157
|
+
return {
|
|
158
|
+
data_source: 'live_api',
|
|
159
|
+
providers,
|
|
160
|
+
public_url: PUBLIC_SURFACE.home,
|
|
161
|
+
note: 'Live results from the GeraHome production API.',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// 2. Bundled real crawled seed (Yerevan providers).
|
|
165
|
+
let rows = homeSeed();
|
|
166
|
+
if (args.category) {
|
|
167
|
+
const cat = args.category.toLowerCase();
|
|
168
|
+
rows = rows.filter((r) => (r.category ?? '').toLowerCase().includes(cat));
|
|
169
|
+
}
|
|
170
|
+
if (args.city) {
|
|
171
|
+
const city = args.city.toLowerCase();
|
|
172
|
+
rows = rows.filter((r) => (r.full_address ?? '').toLowerCase().includes(city) ||
|
|
173
|
+
(r.district ?? '').toLowerCase().includes(city));
|
|
174
|
+
}
|
|
175
|
+
const providers = rows.slice(0, args.limit).map((r, i) => ({
|
|
176
|
+
id: `home-seed-${i}`,
|
|
177
|
+
name: r.business_name,
|
|
178
|
+
category: r.category,
|
|
179
|
+
city: 'Yerevan',
|
|
180
|
+
area: r.district || undefined,
|
|
181
|
+
phone: r.phone_e164 || undefined,
|
|
182
|
+
contact: r.email_or_whatsapp || undefined,
|
|
183
|
+
languages: r.languages_spoken || undefined,
|
|
184
|
+
years_in_business: r.years_in_business || undefined,
|
|
185
|
+
why_qualified: r.why_qualified || undefined,
|
|
186
|
+
source_url: r.source_evidence_url || undefined,
|
|
187
|
+
}));
|
|
188
|
+
return {
|
|
189
|
+
data_source: 'bundled_seed',
|
|
190
|
+
providers,
|
|
191
|
+
public_url: PUBLIC_SURFACE.home,
|
|
192
|
+
note: 'Bundled real crawled providers (Yerevan, sourced from List.am/Spyur). The live GeraHome API was unreachable or empty; these are verified seed records.',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// ─── JOBS ─────────────────────────────────────────────────────────────────────
|
|
196
|
+
export async function searchJobs(args) {
|
|
197
|
+
// 1. Best-effort live GeraJobs API.
|
|
198
|
+
const q = new URLSearchParams();
|
|
199
|
+
if (args.query)
|
|
200
|
+
q.set('query', args.query);
|
|
201
|
+
if (args.location)
|
|
202
|
+
q.set('location', args.location);
|
|
203
|
+
q.set('limit', String(args.limit));
|
|
204
|
+
const live = (await fetchJson(`${MARKETPLACE_API.jobs}/api/v1/jobs?${q.toString()}`, { headers: { 'x-tenant-id': 'GB' } }));
|
|
205
|
+
const liveRows = (live?.data ?? live?.items ?? null);
|
|
206
|
+
if (Array.isArray(liveRows) && liveRows.length > 0) {
|
|
207
|
+
const jobs = liveRows.slice(0, args.limit).map((r, i) => ({
|
|
208
|
+
id: String(r.id ?? `job-live-${i}`),
|
|
209
|
+
title: String(r.title ?? 'Job'),
|
|
210
|
+
organization: String(r.company_name ?? r.company ?? r.organization ?? ''),
|
|
211
|
+
location: String(r.location ?? args.location ?? ''),
|
|
212
|
+
category: r.category ? String(r.category) : undefined,
|
|
213
|
+
job_type: r.job_type ? String(r.job_type) : undefined,
|
|
214
|
+
salary: r.salary_min || r.salary_max
|
|
215
|
+
? `${r.salary_min ?? ''}-${r.salary_max ?? ''}`
|
|
216
|
+
: undefined,
|
|
217
|
+
}));
|
|
218
|
+
return {
|
|
219
|
+
data_source: 'live_api',
|
|
220
|
+
jobs,
|
|
221
|
+
public_url: PUBLIC_SURFACE.jobs,
|
|
222
|
+
note: 'Live results from the GeraJobs production API.',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// 2. Bundled real seed — recruitment-agency supply partners. These are
|
|
226
|
+
// organizations that supply jobs, not individual postings, so we present
|
|
227
|
+
// them honestly as "recruiters / job sources", not as live vacancies.
|
|
228
|
+
let rows = jobsSeed();
|
|
229
|
+
if (args.query) {
|
|
230
|
+
const kw = args.query.toLowerCase();
|
|
231
|
+
rows = rows.filter((r) => (r.name ?? '').toLowerCase().includes(kw) ||
|
|
232
|
+
(r.notes ?? '').toLowerCase().includes(kw));
|
|
233
|
+
}
|
|
234
|
+
if (args.location) {
|
|
235
|
+
const loc = args.location.toLowerCase();
|
|
236
|
+
rows = rows.filter((r) => (r.city ?? '').toLowerCase().includes(loc) ||
|
|
237
|
+
(r.country ?? '').toLowerCase().includes(loc));
|
|
238
|
+
}
|
|
239
|
+
const jobs = rows.slice(0, args.limit).map((r, i) => ({
|
|
240
|
+
id: `job-seed-${i}`,
|
|
241
|
+
title: `Recruiter / job source: ${r.name}`,
|
|
242
|
+
organization: r.name,
|
|
243
|
+
location: [r.city, r.country].filter(Boolean).join(', '),
|
|
244
|
+
website: r.website || undefined,
|
|
245
|
+
notes: r.notes || undefined,
|
|
246
|
+
}));
|
|
247
|
+
return {
|
|
248
|
+
data_source: 'bundled_seed',
|
|
249
|
+
jobs,
|
|
250
|
+
public_url: PUBLIC_SURFACE.jobs,
|
|
251
|
+
note: 'The live GeraJobs API was unreachable or has no matching vacancies. These are real recruitment-agency / job-board supply partners (not individual vacancies) — use them as the place to find live openings.',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// ─── RESTAURANTS (live FHRS open data) ────────────────────────────────────────
|
|
255
|
+
export async function searchRestaurants(args) {
|
|
256
|
+
// LIVE: UK Food Standards Agency FHRS open API — real, no auth.
|
|
257
|
+
const q = new URLSearchParams();
|
|
258
|
+
if (args.name)
|
|
259
|
+
q.set('name', args.name);
|
|
260
|
+
if (args.city)
|
|
261
|
+
q.set('address', args.city);
|
|
262
|
+
// FHRS BusinessTypeId 1 = "Restaurant/Cafe/Canteen", 7843 = pub/bar; we
|
|
263
|
+
// leave type open and filter to food-serving types client-side.
|
|
264
|
+
q.set('pageSize', String(Math.min(args.limit * 3, 100)));
|
|
265
|
+
const data = (await fetchJson(`https://api.ratings.food.gov.uk/Establishments?${q.toString()}`, { headers: { 'x-api-version': '2', accept: 'application/json' }, timeoutMs: 6000 }));
|
|
266
|
+
if (data && Array.isArray(data.establishments)) {
|
|
267
|
+
let rows = data.establishments;
|
|
268
|
+
if (typeof args.min_rating === 'number') {
|
|
269
|
+
rows = rows.filter((r) => {
|
|
270
|
+
const v = parseInt(String(r.RatingValue), 10);
|
|
271
|
+
return !Number.isNaN(v) && v >= args.min_rating;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const restaurants = rows.slice(0, args.limit).map((r) => {
|
|
275
|
+
const geo = (r.geocode ?? {});
|
|
276
|
+
return {
|
|
277
|
+
id: `fhrs-${r.FHRSID}`,
|
|
278
|
+
name: String(r.BusinessName ?? ''),
|
|
279
|
+
business_type: r.BusinessType ? String(r.BusinessType) : undefined,
|
|
280
|
+
address: [r.AddressLine1, r.AddressLine2, r.AddressLine3, r.AddressLine4]
|
|
281
|
+
.filter(Boolean)
|
|
282
|
+
.map(String)
|
|
283
|
+
.join(', '),
|
|
284
|
+
postcode: r.PostCode ? String(r.PostCode) : undefined,
|
|
285
|
+
city: args.city,
|
|
286
|
+
hygiene_rating: r.RatingValue ? String(r.RatingValue) : undefined,
|
|
287
|
+
rating_date: r.RatingDate ? String(r.RatingDate).slice(0, 10) : undefined,
|
|
288
|
+
phone: r.Phone ? String(r.Phone) : undefined,
|
|
289
|
+
local_authority: r.LocalAuthorityName ? String(r.LocalAuthorityName) : undefined,
|
|
290
|
+
lat: geo.latitude ? String(geo.latitude) : undefined,
|
|
291
|
+
lng: geo.longitude ? String(geo.longitude) : undefined,
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
data_source: 'live_fhrs',
|
|
296
|
+
restaurants,
|
|
297
|
+
public_url: PUBLIC_SURFACE.eats,
|
|
298
|
+
note: 'Live UK restaurant data with current Food Hygiene Ratings from the Food Standards Agency (api.ratings.food.gov.uk). GeraEats lists and delivers from establishments like these.',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
data_source: 'live_fhrs',
|
|
303
|
+
restaurants: [],
|
|
304
|
+
public_url: PUBLIC_SURFACE.eats,
|
|
305
|
+
note: 'The FHRS open data API was unreachable. No restaurants returned — try again or refine the city/name.',
|
|
306
|
+
};
|
|
307
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gera-services/mcp-gera-nexus
|
|
3
|
+
*
|
|
4
|
+
* MCP server for GeraNexus — the agentic-commerce ACTION rails by Gera Systems.
|
|
5
|
+
* Re-exports the server factory for programmatic / HTTP use.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export { server, createServer, registerTools, main } from './server.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gera-services/mcp-gera-nexus
|
|
3
|
+
*
|
|
4
|
+
* MCP server for GeraNexus — the agentic-commerce ACTION rails by Gera Systems.
|
|
5
|
+
* Re-exports the server factory for programmatic / HTTP use.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export { server, createServer, registerTools, main } from './server.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent-first action intents for GeraNexus.
|
|
3
|
+
*
|
|
4
|
+
* An "intent" is a structured, human-confirmable lead. It is explicitly NOT an
|
|
5
|
+
* executed transaction: no payment is taken, no provider is committed. The
|
|
6
|
+
* intent records what the agent + user want, assigns a tracking id, builds a
|
|
7
|
+
* deep-link the human opens to confirm/pay on the real Gera surface, and
|
|
8
|
+
* appends to an in-process audit trail.
|
|
9
|
+
*
|
|
10
|
+
* This mirrors the GeraNexus protocol's ConsentGate / human-in-the-loop step
|
|
11
|
+
* (services/gera-nexus protocol.service.ts) — the spend-threshold gate that
|
|
12
|
+
* routes to out-of-band human approval. Here, EVERY action is gated, because
|
|
13
|
+
* an MCP server acting for a user must never silently spend their money.
|
|
14
|
+
*
|
|
15
|
+
* Storage is in-process (Map). It is intentionally ephemeral: a real
|
|
16
|
+
* deployment would persist these to the GeraNexus backend's audit log. We do
|
|
17
|
+
* not pretend otherwise — get_action_status only sees intents created in this
|
|
18
|
+
* same process/session.
|
|
19
|
+
*/
|
|
20
|
+
export type ActionKind = 'home_booking' | 'job_application' | 'restaurant_order';
|
|
21
|
+
export type IntentStatus = 'pending_human_confirmation' | 'expired' | 'unknown';
|
|
22
|
+
export interface ActionIntent {
|
|
23
|
+
intent_id: string;
|
|
24
|
+
kind: ActionKind;
|
|
25
|
+
status: IntentStatus;
|
|
26
|
+
created_at: string;
|
|
27
|
+
expires_at: string;
|
|
28
|
+
summary: string;
|
|
29
|
+
target: {
|
|
30
|
+
marketplace: 'GeraHome' | 'GeraJobs' | 'GeraEats';
|
|
31
|
+
provider_id?: string;
|
|
32
|
+
provider_name?: string;
|
|
33
|
+
};
|
|
34
|
+
details: Record<string, unknown>;
|
|
35
|
+
confirmation_url: string;
|
|
36
|
+
audit_signature: string;
|
|
37
|
+
audit_trail: Array<{
|
|
38
|
+
at: string;
|
|
39
|
+
event: string;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
export declare function createIntent(input: {
|
|
43
|
+
kind: ActionKind;
|
|
44
|
+
marketplace: ActionIntent['target']['marketplace'];
|
|
45
|
+
provider_id?: string;
|
|
46
|
+
provider_name?: string;
|
|
47
|
+
summary: string;
|
|
48
|
+
details: Record<string, unknown>;
|
|
49
|
+
}): ActionIntent;
|
|
50
|
+
export declare function getIntent(id: string): ActionIntent | null;
|
package/dist/intents.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent-first action intents for GeraNexus.
|
|
3
|
+
*
|
|
4
|
+
* An "intent" is a structured, human-confirmable lead. It is explicitly NOT an
|
|
5
|
+
* executed transaction: no payment is taken, no provider is committed. The
|
|
6
|
+
* intent records what the agent + user want, assigns a tracking id, builds a
|
|
7
|
+
* deep-link the human opens to confirm/pay on the real Gera surface, and
|
|
8
|
+
* appends to an in-process audit trail.
|
|
9
|
+
*
|
|
10
|
+
* This mirrors the GeraNexus protocol's ConsentGate / human-in-the-loop step
|
|
11
|
+
* (services/gera-nexus protocol.service.ts) — the spend-threshold gate that
|
|
12
|
+
* routes to out-of-band human approval. Here, EVERY action is gated, because
|
|
13
|
+
* an MCP server acting for a user must never silently spend their money.
|
|
14
|
+
*
|
|
15
|
+
* Storage is in-process (Map). It is intentionally ephemeral: a real
|
|
16
|
+
* deployment would persist these to the GeraNexus backend's audit log. We do
|
|
17
|
+
* not pretend otherwise — get_action_status only sees intents created in this
|
|
18
|
+
* same process/session.
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
21
|
+
const store = new Map();
|
|
22
|
+
const PUBLIC_SURFACE = {
|
|
23
|
+
GeraHome: 'https://gerahome.com',
|
|
24
|
+
GeraJobs: 'https://gerajobs.com',
|
|
25
|
+
GeraEats: 'https://geraeats.com',
|
|
26
|
+
};
|
|
27
|
+
const INTENT_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
28
|
+
function signature(payload) {
|
|
29
|
+
return ('sha256:' +
|
|
30
|
+
createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 32));
|
|
31
|
+
}
|
|
32
|
+
export function createIntent(input) {
|
|
33
|
+
const id = `intent_${randomUUID()}`;
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const created_at = now.toISOString();
|
|
36
|
+
const expires_at = new Date(now.getTime() + INTENT_TTL_MS).toISOString();
|
|
37
|
+
// Deep-link to the relevant public surface, carrying the intent id so the
|
|
38
|
+
// human lands on the right place to review and confirm. The path differs per
|
|
39
|
+
// marketplace and matches the real public surfaces.
|
|
40
|
+
const base = PUBLIC_SURFACE[input.marketplace];
|
|
41
|
+
const pathByKind = {
|
|
42
|
+
home_booking: '/book',
|
|
43
|
+
job_application: '/apply',
|
|
44
|
+
restaurant_order: '/order',
|
|
45
|
+
};
|
|
46
|
+
const params = new URLSearchParams({ intent: id, source: 'mcp-gera-nexus' });
|
|
47
|
+
if (input.provider_id)
|
|
48
|
+
params.set('ref', input.provider_id);
|
|
49
|
+
const confirmation_url = `${base}${pathByKind[input.kind]}?${params.toString()}`;
|
|
50
|
+
const corePayload = {
|
|
51
|
+
id,
|
|
52
|
+
kind: input.kind,
|
|
53
|
+
marketplace: input.marketplace,
|
|
54
|
+
provider_id: input.provider_id,
|
|
55
|
+
details: input.details,
|
|
56
|
+
created_at,
|
|
57
|
+
};
|
|
58
|
+
const intent = {
|
|
59
|
+
intent_id: id,
|
|
60
|
+
kind: input.kind,
|
|
61
|
+
status: 'pending_human_confirmation',
|
|
62
|
+
created_at,
|
|
63
|
+
expires_at,
|
|
64
|
+
summary: input.summary,
|
|
65
|
+
target: {
|
|
66
|
+
marketplace: input.marketplace,
|
|
67
|
+
provider_id: input.provider_id,
|
|
68
|
+
provider_name: input.provider_name,
|
|
69
|
+
},
|
|
70
|
+
details: input.details,
|
|
71
|
+
confirmation_url,
|
|
72
|
+
audit_signature: signature(corePayload),
|
|
73
|
+
audit_trail: [
|
|
74
|
+
{ at: created_at, event: 'intent_created_via_mcp (no payment taken)' },
|
|
75
|
+
{ at: created_at, event: 'awaiting_human_confirmation' },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
store.set(id, intent);
|
|
79
|
+
return intent;
|
|
80
|
+
}
|
|
81
|
+
export function getIntent(id) {
|
|
82
|
+
const intent = store.get(id);
|
|
83
|
+
if (!intent)
|
|
84
|
+
return null;
|
|
85
|
+
if (intent.status === 'pending_human_confirmation' &&
|
|
86
|
+
Date.now() > new Date(intent.expires_at).getTime()) {
|
|
87
|
+
intent.status = 'expired';
|
|
88
|
+
intent.audit_trail.push({
|
|
89
|
+
at: new Date().toISOString(),
|
|
90
|
+
event: 'intent_expired_unconfirmed',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return intent;
|
|
94
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeraNexus MCP Server (stdio) — the agent ACTION rails.
|
|
3
|
+
*
|
|
4
|
+
* GeraNexus is "the Agentic Commerce Protocol": the transactional layer AI
|
|
5
|
+
* agents use to DO real-world things across Gera's marketplaces on a user's
|
|
6
|
+
* behalf. This MCP server is the agent-facing front door to that protocol —
|
|
7
|
+
* the "do-this-for-me-safely" layer.
|
|
8
|
+
*
|
|
9
|
+
* Two kinds of tools:
|
|
10
|
+
* • SEARCH tools (read-only) — find real home-service providers, jobs, and
|
|
11
|
+
* restaurants. Backed by live Gera APIs where reachable, the UK FSA FHRS
|
|
12
|
+
* open data for restaurants, and bundled real crawled seed data otherwise.
|
|
13
|
+
* Every result is labelled with its data_source.
|
|
14
|
+
* • ACTION tools (consent-first) — create_booking_intent records a
|
|
15
|
+
* structured, human-confirmable INTENT/lead and returns a confirmation id
|
|
16
|
+
* plus a deep-link the human opens to confirm. It does NOT charge, does NOT
|
|
17
|
+
* commit a provider, and NEVER reports a completed transaction. This is the
|
|
18
|
+
* ConsentGate / human-in-the-loop step of the GeraNexus protocol, applied
|
|
19
|
+
* to every action.
|
|
20
|
+
*
|
|
21
|
+
* Honesty contract: an action that cannot truly complete returns a clearly
|
|
22
|
+
* labelled "intent + next-step link", never a faked confirmation.
|
|
23
|
+
*
|
|
24
|
+
* Product: GeraNexus — agentic commerce protocol (a Gera Systems product).
|
|
25
|
+
*/
|
|
26
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
27
|
+
export declare function registerTools(server: McpServer): void;
|
|
28
|
+
/** Construct a fully-configured GeraNexus McpServer (all tools registered). */
|
|
29
|
+
export declare function createServer(): McpServer;
|
|
30
|
+
export declare const server: McpServer;
|
|
31
|
+
export declare function main(): Promise<void>;
|