@haven_ai/mcp 0.0.0-dev.202609031523.fd49e1a
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 +197 -0
- package/dist/cli.cjs +955 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +953 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +922 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +252 -0
- package/dist/index.d.ts +252 -0
- package/dist/index.js +905 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { composeDescription, toolDescriptions as toolDescriptions$1, HavenClient, isSupportedNodeVersion, unsupportedNodeVersionMessage, HavenPaymentStateError, HavenSigningError, HavenApiError, AgentPaymentNextAction, HavenError, verifyPaymentReceipt, discoverMerchantMcpUrl, sameUrl } from '@haven_ai/sdk';
|
|
5
|
+
import { readFile, mkdir, writeFile, stat } from 'fs/promises';
|
|
6
|
+
import { z } from 'zod/v3';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { resolve, dirname } from 'path';
|
|
9
|
+
|
|
10
|
+
async function loadCredentials(source = process.env.HAVEN_CREDENTIALS) {
|
|
11
|
+
if (typeof source === "string") {
|
|
12
|
+
return loadCredentialsFromFile(source);
|
|
13
|
+
}
|
|
14
|
+
if (source?.credentialsPath) {
|
|
15
|
+
return loadCredentialsFromFile(source.credentialsPath);
|
|
16
|
+
}
|
|
17
|
+
if (source?.identityPath || source?.signerPath) {
|
|
18
|
+
if (!source.identityPath || !source.signerPath) {
|
|
19
|
+
throw new Error("Haven split credentials require both --identity and --signer paths.");
|
|
20
|
+
}
|
|
21
|
+
return loadCredentialsFromSplitFiles(source.identityPath, source.signerPath);
|
|
22
|
+
}
|
|
23
|
+
const envCreds = loadCredentialsFromEnv();
|
|
24
|
+
if (envCreds) return envCreds;
|
|
25
|
+
throw new Error(
|
|
26
|
+
"No Haven credentials found. Set HAVEN_CREDENTIALS to a Haven agent credential JSON file, pass --credentials <path>, or set HAVEN_API_KEY and HAVEN_DELEGATE_KEY environment variables."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
async function loadCredentialsFromFile(path) {
|
|
30
|
+
let rawText;
|
|
31
|
+
try {
|
|
32
|
+
rawText = await readFile(path, "utf8");
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw new Error(`Could not read Haven credentials at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
}
|
|
36
|
+
await warnIfCredentialFilePermissive(path);
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = JSON.parse(rawText);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error("Haven credentials must be JSON with api_key and delegate_key fields.");
|
|
42
|
+
}
|
|
43
|
+
const apiKey = stringField(raw.api_key ?? raw.apiKey);
|
|
44
|
+
const delegateKey = stringField(raw.delegate_key ?? raw.delegateKey);
|
|
45
|
+
if (!apiKey) {
|
|
46
|
+
throw new Error("Haven credentials are missing api_key.");
|
|
47
|
+
}
|
|
48
|
+
if (!delegateKey) {
|
|
49
|
+
throw new Error("Haven MCP requires delegate_key so payments can be signed locally.");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
apiKey,
|
|
53
|
+
delegateKey,
|
|
54
|
+
agentId: stringField(raw.agent_id ?? raw.agentId),
|
|
55
|
+
safeAddress: stringField(raw.safe_address ?? raw.safeAddress),
|
|
56
|
+
delegateAddress: stringField(raw.delegate_address ?? raw.delegateAddress),
|
|
57
|
+
chainId: numberField(raw.chain_id ?? raw.chainId),
|
|
58
|
+
network: stringField(raw.network),
|
|
59
|
+
apiUrl: stringField(raw.api_url ?? raw.apiUrl),
|
|
60
|
+
allowanceSummary: allowanceSummaryField(raw.allowance_summary ?? raw.allowanceSummary ?? raw.agent_budget ?? raw.agentBudget),
|
|
61
|
+
sourcePath: path
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function loadCredentialsFromSplitFiles(identityPath, signerPath) {
|
|
65
|
+
const identity = await readJsonFile(identityPath, "Haven identity credentials");
|
|
66
|
+
const signer = await readJsonFile(signerPath, "Haven signer credentials");
|
|
67
|
+
await warnIfCredentialFilePermissive(identityPath);
|
|
68
|
+
await warnIfCredentialFilePermissive(signerPath);
|
|
69
|
+
const apiKey = stringField(identity.api_key ?? identity.apiKey);
|
|
70
|
+
const delegateKey = stringField(signer.delegate_key ?? signer.delegateKey);
|
|
71
|
+
if (!apiKey) {
|
|
72
|
+
throw new Error("Haven identity credentials are missing api_key.");
|
|
73
|
+
}
|
|
74
|
+
if (!delegateKey) {
|
|
75
|
+
throw new Error("Haven signer credentials are missing delegate_key.");
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
apiKey,
|
|
79
|
+
delegateKey,
|
|
80
|
+
agentId: matchingStringField(
|
|
81
|
+
"agent_id",
|
|
82
|
+
identity.agent_id ?? identity.agentId,
|
|
83
|
+
signer.agent_id ?? signer.agentId
|
|
84
|
+
),
|
|
85
|
+
safeAddress: matchingStringField(
|
|
86
|
+
"safe_address",
|
|
87
|
+
identity.safe_address ?? identity.safeAddress,
|
|
88
|
+
signer.safe_address ?? signer.safeAddress,
|
|
89
|
+
{ caseInsensitive: true }
|
|
90
|
+
),
|
|
91
|
+
delegateAddress: matchingStringField(
|
|
92
|
+
"delegate_address",
|
|
93
|
+
signer.delegate_address ?? signer.delegateAddress,
|
|
94
|
+
identity.delegate_address ?? identity.delegateAddress,
|
|
95
|
+
{ caseInsensitive: true }
|
|
96
|
+
),
|
|
97
|
+
chainId: matchingNumberField(
|
|
98
|
+
"chain_id",
|
|
99
|
+
identity.chain_id ?? identity.chainId,
|
|
100
|
+
signer.chain_id ?? signer.chainId
|
|
101
|
+
),
|
|
102
|
+
network: matchingStringField(
|
|
103
|
+
"network",
|
|
104
|
+
identity.network,
|
|
105
|
+
signer.network,
|
|
106
|
+
{ caseInsensitive: true }
|
|
107
|
+
),
|
|
108
|
+
apiUrl: stringField(identity.api_url ?? identity.apiUrl),
|
|
109
|
+
allowanceSummary: allowanceSummaryField(
|
|
110
|
+
identity.allowance_summary ?? identity.allowanceSummary ?? identity.agent_budget ?? identity.agentBudget
|
|
111
|
+
),
|
|
112
|
+
sourcePath: identityPath,
|
|
113
|
+
identityPath,
|
|
114
|
+
signerPath
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function readJsonFile(path, label) {
|
|
118
|
+
let rawText;
|
|
119
|
+
try {
|
|
120
|
+
rawText = await readFile(path, "utf8");
|
|
121
|
+
} catch (err) {
|
|
122
|
+
throw new Error(`Could not read ${label} at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(rawText);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error(`${label} must be JSON.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function loadCredentialsFromEnv() {
|
|
131
|
+
const apiKey = stringField(process.env.HAVEN_API_KEY);
|
|
132
|
+
const delegateKey = stringField(process.env.HAVEN_DELEGATE_KEY);
|
|
133
|
+
if (!apiKey && !delegateKey) return null;
|
|
134
|
+
if (!apiKey) {
|
|
135
|
+
throw new Error("HAVEN_DELEGATE_KEY is set but HAVEN_API_KEY is missing.");
|
|
136
|
+
}
|
|
137
|
+
if (!delegateKey) {
|
|
138
|
+
throw new Error("HAVEN_API_KEY is set but HAVEN_DELEGATE_KEY is missing. Haven MCP requires a delegate key so payments can be signed locally.");
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
apiKey,
|
|
142
|
+
delegateKey,
|
|
143
|
+
agentId: stringField(process.env.HAVEN_AGENT_ID),
|
|
144
|
+
safeAddress: stringField(process.env.HAVEN_SAFE_ADDRESS),
|
|
145
|
+
chainId: numberField(process.env.HAVEN_CHAIN_ID),
|
|
146
|
+
network: stringField(process.env.HAVEN_NETWORK),
|
|
147
|
+
apiUrl: stringField(process.env.HAVEN_API_URL)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function stringField(value) {
|
|
151
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
152
|
+
}
|
|
153
|
+
function numberField(value) {
|
|
154
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
155
|
+
if (typeof value === "string" && value.trim() && /^\d+$/.test(value.trim())) return Number(value.trim());
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
function matchingStringField(label, preferred, fallback, options = {}) {
|
|
159
|
+
const preferredValue = stringField(preferred);
|
|
160
|
+
const fallbackValue = stringField(fallback);
|
|
161
|
+
if (preferredValue && fallbackValue && !sameCredentialValue(preferredValue, fallbackValue, options.caseInsensitive)) {
|
|
162
|
+
throw mismatchedSplitCredentialError(label);
|
|
163
|
+
}
|
|
164
|
+
return preferredValue ?? fallbackValue;
|
|
165
|
+
}
|
|
166
|
+
function matchingNumberField(label, preferred, fallback) {
|
|
167
|
+
const preferredValue = numberField(preferred);
|
|
168
|
+
const fallbackValue = numberField(fallback);
|
|
169
|
+
if (preferredValue !== void 0 && fallbackValue !== void 0 && preferredValue !== fallbackValue) {
|
|
170
|
+
throw mismatchedSplitCredentialError(label);
|
|
171
|
+
}
|
|
172
|
+
return preferredValue ?? fallbackValue;
|
|
173
|
+
}
|
|
174
|
+
function sameCredentialValue(first, second, caseInsensitive = false) {
|
|
175
|
+
return caseInsensitive ? first.toLowerCase() === second.toLowerCase() : first === second;
|
|
176
|
+
}
|
|
177
|
+
function mismatchedSplitCredentialError(label) {
|
|
178
|
+
return new Error(
|
|
179
|
+
`Haven split credentials have mismatched ${label} values. Recreate the identity and signer credential files from the same Haven agent.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function allowanceSummaryField(value) {
|
|
183
|
+
if (!Array.isArray(value)) return void 0;
|
|
184
|
+
const allowances = value.flatMap((item) => {
|
|
185
|
+
if (!item || typeof item !== "object") return [];
|
|
186
|
+
const raw = item;
|
|
187
|
+
const token = stringField(raw.token ?? raw.token_symbol ?? raw.tokenSymbol);
|
|
188
|
+
const amount = stringField(raw.amount ?? raw.allowance_amount ?? raw.allowanceAmount);
|
|
189
|
+
const reset = raw.resetMinutes ?? raw.reset_minutes ?? raw.reset_period_min ?? raw.resetPeriodMin;
|
|
190
|
+
if (!token || !amount) return [];
|
|
191
|
+
return [{
|
|
192
|
+
token,
|
|
193
|
+
amount,
|
|
194
|
+
resetMinutes: reset === null ? null : numberField(reset) ?? null
|
|
195
|
+
}];
|
|
196
|
+
});
|
|
197
|
+
return allowances.length > 0 ? allowances : void 0;
|
|
198
|
+
}
|
|
199
|
+
async function warnIfCredentialFilePermissive(path, log = (message) => process.stderr.write(`${message}
|
|
200
|
+
`), platform = process.platform) {
|
|
201
|
+
if (platform === "win32") return;
|
|
202
|
+
let mode;
|
|
203
|
+
try {
|
|
204
|
+
const stats = await stat(path);
|
|
205
|
+
mode = stats.mode;
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const groupOrOther = mode & 63;
|
|
210
|
+
if (groupOrOther !== 0) {
|
|
211
|
+
const octal = (mode & 511).toString(8).padStart(4, "0");
|
|
212
|
+
log(
|
|
213
|
+
`haven-mcp: warning: credential file at ${path} is readable beyond the owner (mode ${octal}). Run: chmod 600 ${path}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
var headersSchema = z.record(z.string(), z.string()).optional();
|
|
218
|
+
var toolSchemas = {
|
|
219
|
+
haven_send: {
|
|
220
|
+
asset: z.enum(["ETH", "USDC"]),
|
|
221
|
+
recipient: z.string().min(1),
|
|
222
|
+
amount: z.string().min(1),
|
|
223
|
+
idempotencyKey: z.string().optional()
|
|
224
|
+
},
|
|
225
|
+
haven_pay_mcp_tool: {
|
|
226
|
+
merchant_url: z.string().url(),
|
|
227
|
+
tool_name: z.string().min(1),
|
|
228
|
+
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
229
|
+
idempotencyKey: z.string().optional()
|
|
230
|
+
},
|
|
231
|
+
haven_quote_x402: {
|
|
232
|
+
url: z.string().url(),
|
|
233
|
+
method: z.string().optional(),
|
|
234
|
+
headers: headersSchema,
|
|
235
|
+
body: z.string().optional(),
|
|
236
|
+
idempotencyKey: z.string().optional()
|
|
237
|
+
},
|
|
238
|
+
haven_pay_x402_quote: {
|
|
239
|
+
quote: z.unknown(),
|
|
240
|
+
idempotencyKey: z.string().optional()
|
|
241
|
+
},
|
|
242
|
+
haven_pay_x402: {
|
|
243
|
+
url: z.string().url(),
|
|
244
|
+
method: z.string().optional(),
|
|
245
|
+
headers: headersSchema,
|
|
246
|
+
body: z.string().optional(),
|
|
247
|
+
idempotencyKey: z.string().optional()
|
|
248
|
+
},
|
|
249
|
+
haven_resume_x402_payment: {
|
|
250
|
+
payment_id: z.string().optional(),
|
|
251
|
+
resume_state: z.unknown().optional()
|
|
252
|
+
},
|
|
253
|
+
haven_get_payment_status: {
|
|
254
|
+
payment_id: z.string()
|
|
255
|
+
},
|
|
256
|
+
haven_get_resume_state: {
|
|
257
|
+
payment_id: z.string()
|
|
258
|
+
},
|
|
259
|
+
haven_get_agent: {},
|
|
260
|
+
haven_get_allowances: {},
|
|
261
|
+
haven_sweep_delegate: {},
|
|
262
|
+
haven_discover_tools: {
|
|
263
|
+
category: z.string().optional(),
|
|
264
|
+
search: z.string().optional(),
|
|
265
|
+
rail: z.enum(["x402", "mpp"]).optional(),
|
|
266
|
+
verified: z.enum(["any", "verified", "operator"]).optional()
|
|
267
|
+
},
|
|
268
|
+
haven_submit_catalog_entry: {
|
|
269
|
+
resource_url: z.string().min(1),
|
|
270
|
+
website: z.string().optional()
|
|
271
|
+
},
|
|
272
|
+
haven_list_receipts: {
|
|
273
|
+
limit: z.number().int().min(1).max(100).optional()
|
|
274
|
+
},
|
|
275
|
+
haven_verify_receipt: {
|
|
276
|
+
receipt: z.unknown()
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var toolDescriptions = {
|
|
280
|
+
haven_send: composeDescription(toolDescriptions$1.send),
|
|
281
|
+
haven_pay_mcp_tool: composeDescription(toolDescriptions$1.payMcpTool),
|
|
282
|
+
haven_quote_x402: composeDescription(toolDescriptions$1.quoteX402),
|
|
283
|
+
haven_pay_x402_quote: composeDescription(toolDescriptions$1.payX402),
|
|
284
|
+
haven_pay_x402: composeDescription(toolDescriptions$1.payX402OneShot),
|
|
285
|
+
haven_resume_x402_payment: composeDescription(toolDescriptions$1.resumeX402),
|
|
286
|
+
haven_get_payment_status: composeDescription(toolDescriptions$1.getPaymentStatus),
|
|
287
|
+
haven_get_resume_state: composeDescription(toolDescriptions$1.getResumeState),
|
|
288
|
+
haven_get_agent: composeDescription(toolDescriptions$1.getAgent),
|
|
289
|
+
haven_get_allowances: composeDescription(toolDescriptions$1.getAllowances),
|
|
290
|
+
haven_sweep_delegate: composeDescription(toolDescriptions$1.sweep_delegate),
|
|
291
|
+
haven_discover_tools: composeDescription(toolDescriptions$1.discoverTools),
|
|
292
|
+
haven_submit_catalog_entry: composeDescription(toolDescriptions$1.submitCatalogEntry),
|
|
293
|
+
haven_list_receipts: composeDescription(toolDescriptions$1.listReceipts),
|
|
294
|
+
haven_verify_receipt: composeDescription(toolDescriptions$1.verifyReceipt)
|
|
295
|
+
};
|
|
296
|
+
function createToolHandlers(haven) {
|
|
297
|
+
return {
|
|
298
|
+
haven_send: async (input) => {
|
|
299
|
+
return runTool(async () => {
|
|
300
|
+
const args = objectInput("haven_send", input);
|
|
301
|
+
try {
|
|
302
|
+
const result = await haven.pay({
|
|
303
|
+
token: args.asset,
|
|
304
|
+
amount: args.amount,
|
|
305
|
+
to: args.recipient,
|
|
306
|
+
// #1207: was accepted by the schema but silently dropped — now
|
|
307
|
+
// carried to the backend's replay contract.
|
|
308
|
+
idempotencyKey: typeof args.idempotencyKey === "string" ? args.idempotencyKey : void 0
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
payment_id: result.paymentId,
|
|
312
|
+
status: result.status,
|
|
313
|
+
tx_hash: result.txHash ?? null,
|
|
314
|
+
asset: args.asset,
|
|
315
|
+
amount: args.amount,
|
|
316
|
+
recipient: args.recipient
|
|
317
|
+
};
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (err instanceof HavenPaymentStateError && isPendingApproval(err.status)) {
|
|
320
|
+
return {
|
|
321
|
+
payment_id: err.paymentId,
|
|
322
|
+
status: "pending_approval",
|
|
323
|
+
next_action: "stop_and_tell_user",
|
|
324
|
+
message: "This payment is not payable and nothing is queued for anyone to approve \u2014 stop and tell the user, then ask the wallet owner to grant or raise the agent budget in Haven. Do not retry, re-sign, or poll.",
|
|
325
|
+
asset: args.asset,
|
|
326
|
+
amount: args.amount,
|
|
327
|
+
recipient: args.recipient
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
haven_pay_mcp_tool: async (input) => {
|
|
335
|
+
return runTool(async () => {
|
|
336
|
+
const args = objectInput("haven_pay_mcp_tool", input);
|
|
337
|
+
const envelope = buildMcpToolsCallEnvelope(args.tool_name, args.arguments);
|
|
338
|
+
const init = {
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers: { "Content-Type": "application/json" },
|
|
341
|
+
body: JSON.stringify(envelope)
|
|
342
|
+
};
|
|
343
|
+
let merchantUrl = args.merchant_url;
|
|
344
|
+
const idempotencyKey = args.idempotencyKey;
|
|
345
|
+
const attempt = () => haven.fetch(merchantUrl, init, { idempotencyKey });
|
|
346
|
+
let response = await attempt();
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
const discovered = await discoverMerchantMcpUrl(merchantUrl);
|
|
349
|
+
if (!discovered || sameUrl(discovered, merchantUrl)) {
|
|
350
|
+
throw discoveryMissError(response, merchantUrl, discovered);
|
|
351
|
+
}
|
|
352
|
+
const inputUrl = merchantUrl;
|
|
353
|
+
merchantUrl = discovered;
|
|
354
|
+
const retryResponse = await attempt();
|
|
355
|
+
if (!retryResponse.ok) {
|
|
356
|
+
throw discoveryMissError(retryResponse, merchantUrl, discovered, inputUrl);
|
|
357
|
+
}
|
|
358
|
+
response = retryResponse;
|
|
359
|
+
}
|
|
360
|
+
const payload = await responsePayload(response);
|
|
361
|
+
return {
|
|
362
|
+
...payload,
|
|
363
|
+
// The RESOLVED endpoint (#1271/#1301), not the input as given.
|
|
364
|
+
merchant_url: merchantUrl,
|
|
365
|
+
...merchantUrl !== args.merchant_url ? { merchant_url_discovered_from: args.merchant_url } : {}
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
},
|
|
369
|
+
haven_quote_x402: async (input) => {
|
|
370
|
+
const args = objectInput("haven_quote_x402", input);
|
|
371
|
+
try {
|
|
372
|
+
return { success: true, data: await haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey }) };
|
|
373
|
+
} catch (err) {
|
|
374
|
+
return normalizeError(err);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
haven_pay_x402_quote: async (input) => {
|
|
378
|
+
const args = objectInput("haven_pay_x402_quote", input);
|
|
379
|
+
const quote = args.quote;
|
|
380
|
+
if (!quote || typeof quote !== "object") {
|
|
381
|
+
return wrongTool(
|
|
382
|
+
"WRONG_TOOL",
|
|
383
|
+
"The quote argument is missing or is not a valid x402 quote object. Call haven_quote_x402 first to obtain a quote, or use haven_pay_x402 to handle the full probe \u2192 pay \u2192 retry round trip automatically.",
|
|
384
|
+
"haven_quote_x402"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
if (!quote.paymentRequired) {
|
|
388
|
+
return wrongTool(
|
|
389
|
+
"WRONG_TOOL",
|
|
390
|
+
"The quote is missing the required paymentRequired field. Call haven_quote_x402 first to obtain a valid x402 quote.",
|
|
391
|
+
"haven_quote_x402"
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return runTool(async () => {
|
|
395
|
+
const response = await haven.payX402Quote(args.quote, { idempotencyKey: args.idempotencyKey });
|
|
396
|
+
return responsePayload(response);
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
haven_pay_x402: async (input) => {
|
|
400
|
+
const args = objectInput("haven_pay_x402", input);
|
|
401
|
+
return runTool(async () => {
|
|
402
|
+
const response = await haven.fetch(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey });
|
|
403
|
+
return responsePayload(response);
|
|
404
|
+
});
|
|
405
|
+
},
|
|
406
|
+
haven_resume_x402_payment: async (input) => {
|
|
407
|
+
const args = objectInput("haven_resume_x402_payment", input);
|
|
408
|
+
return runTool(async () => {
|
|
409
|
+
const state = await resumeState(args, "x402");
|
|
410
|
+
const response = await haven.resumeX402Payment(state);
|
|
411
|
+
return responsePayload(response);
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
haven_get_payment_status: async (input) => {
|
|
415
|
+
const args = objectInput("haven_get_payment_status", input);
|
|
416
|
+
return runTool(async () => haven.getPaymentStatusWithPostPurchaseAllowance(args.payment_id));
|
|
417
|
+
},
|
|
418
|
+
haven_get_resume_state: async (input) => {
|
|
419
|
+
const args = objectInput("haven_get_resume_state", input);
|
|
420
|
+
return runTool(async () => haven.getResumeState(args.payment_id));
|
|
421
|
+
},
|
|
422
|
+
haven_get_agent: async () => runTool(async () => haven.getAgentSummary()),
|
|
423
|
+
haven_get_allowances: async () => runTool(async () => haven.getAllowances()),
|
|
424
|
+
haven_sweep_delegate: async () => runTool(async () => haven.sweepDelegate()),
|
|
425
|
+
haven_discover_tools: async (input) => {
|
|
426
|
+
const args = objectInput("haven_discover_tools", input);
|
|
427
|
+
return runTool(async () => {
|
|
428
|
+
const entries = await haven.discoverTools({
|
|
429
|
+
category: typeof args.category === "string" ? args.category : void 0,
|
|
430
|
+
search: typeof args.search === "string" ? args.search : void 0,
|
|
431
|
+
rail: args.rail === "x402" || args.rail === "mpp" ? args.rail : void 0,
|
|
432
|
+
verified: args.verified === "verified" || args.verified === "operator" ? args.verified : void 0
|
|
433
|
+
});
|
|
434
|
+
return entries.map((entry) => ({
|
|
435
|
+
id: entry.id,
|
|
436
|
+
name: entry.name,
|
|
437
|
+
description: entry.description,
|
|
438
|
+
category: entry.category,
|
|
439
|
+
resource_url: entry.resourceUrl,
|
|
440
|
+
rail: entry.rail,
|
|
441
|
+
protocol: entry.protocol,
|
|
442
|
+
tool_name: entry.toolName,
|
|
443
|
+
tool_arguments: entry.toolArguments,
|
|
444
|
+
price_display: entry.priceDisplay,
|
|
445
|
+
price_atomic: entry.priceAtomic,
|
|
446
|
+
asset: entry.asset,
|
|
447
|
+
network: entry.network,
|
|
448
|
+
status: entry.status,
|
|
449
|
+
verified_at: entry.verifiedAt,
|
|
450
|
+
source: entry.source,
|
|
451
|
+
domain_verified: entry.domainVerified,
|
|
452
|
+
verified_payable: entry.verifiedPayable,
|
|
453
|
+
// Which Haven pay tool reaches this entry from the local MCP surface.
|
|
454
|
+
// #1328: the 'mpp' rail's only-ever catalog row (the Haven MPP demo
|
|
455
|
+
// resource) is delisted with the mpp_demo retirement, so this
|
|
456
|
+
// fallback is unreachable today; it stays x402 rather than naming a
|
|
457
|
+
// deleted tool in case a future non-demo 'mpp' rail entry appears.
|
|
458
|
+
suggested_tool: entry.protocol === "mcp" ? "haven_pay_mcp_tool" : "haven_pay_x402"
|
|
459
|
+
}));
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
haven_submit_catalog_entry: async (input) => {
|
|
463
|
+
const args = objectInput("haven_submit_catalog_entry", input);
|
|
464
|
+
return runTool(async () => {
|
|
465
|
+
const submission = await haven.submitCatalogEntry(
|
|
466
|
+
String(args.resource_url),
|
|
467
|
+
typeof args.website === "string" ? { website: args.website } : void 0
|
|
468
|
+
);
|
|
469
|
+
return {
|
|
470
|
+
id: submission.id,
|
|
471
|
+
verify_token: submission.verifyToken,
|
|
472
|
+
status: submission.status
|
|
473
|
+
};
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
haven_list_receipts: async (input) => {
|
|
477
|
+
const args = objectInput("haven_list_receipts", input);
|
|
478
|
+
return runTool(async () => haven.listReceipts({ limit: args.limit }));
|
|
479
|
+
},
|
|
480
|
+
haven_verify_receipt: async (input) => {
|
|
481
|
+
const args = objectInput("haven_verify_receipt", input);
|
|
482
|
+
return runTool(async () => verifyPaymentReceipt(args.receipt));
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
async function resumeState(args, rail) {
|
|
486
|
+
const state = args.resume_state ?? (args.payment_id ? await haven.getResumeState(args.payment_id) : void 0);
|
|
487
|
+
if (!state || typeof state !== "object") {
|
|
488
|
+
throw new HavenApiError(`haven_resume_${rail}_payment requires resume_state or payment_id.`, 400);
|
|
489
|
+
}
|
|
490
|
+
if (state.rail !== rail) {
|
|
491
|
+
throw new HavenApiError(`Resume state is not for the ${rail} rail.`, 409, state);
|
|
492
|
+
}
|
|
493
|
+
return state;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function isPendingApproval(status) {
|
|
497
|
+
return status === "pending" || status === "pending_approval";
|
|
498
|
+
}
|
|
499
|
+
function wrongTool(code, message, suggested_tool) {
|
|
500
|
+
return { success: false, code, message, suggested_tool };
|
|
501
|
+
}
|
|
502
|
+
function buildMcpToolsCallEnvelope(toolName, args) {
|
|
503
|
+
return {
|
|
504
|
+
jsonrpc: "2.0",
|
|
505
|
+
id: `haven-mcp-${Date.now()}`,
|
|
506
|
+
method: "tools/call",
|
|
507
|
+
params: {
|
|
508
|
+
name: toolName,
|
|
509
|
+
arguments: args ?? {}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function objectInput(name, input) {
|
|
514
|
+
return z.object(toolSchemas[name]).parse(input ?? {});
|
|
515
|
+
}
|
|
516
|
+
function requestInit(input) {
|
|
517
|
+
if (!input.method && !input.headers && input.body === void 0) return void 0;
|
|
518
|
+
return {
|
|
519
|
+
method: input.method,
|
|
520
|
+
headers: input.headers,
|
|
521
|
+
body: input.body
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
async function runTool(fn) {
|
|
525
|
+
try {
|
|
526
|
+
return { success: true, data: await fn() };
|
|
527
|
+
} catch (err) {
|
|
528
|
+
return normalizeError(err);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function responsePayload(response) {
|
|
532
|
+
const text = await response.text();
|
|
533
|
+
return {
|
|
534
|
+
status: response.status,
|
|
535
|
+
statusText: response.statusText,
|
|
536
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
537
|
+
body: parseMaybeJson(text)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function parseMaybeJson(text) {
|
|
541
|
+
if (!text) return null;
|
|
542
|
+
try {
|
|
543
|
+
return JSON.parse(text);
|
|
544
|
+
} catch {
|
|
545
|
+
return text;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function discoveryMissError(response, merchantUrl, discovered, discoveredFromUrl) {
|
|
549
|
+
const base = discoveredFromUrl ? `Merchant call to ${merchantUrl} failed with HTTP ${response.status} (at the DISCOVERED endpoint ${merchantUrl}, resolved from ${discoveredFromUrl} via the merchant discovery document).` : `Merchant call to ${merchantUrl} failed with HTTP ${response.status}.`;
|
|
550
|
+
const guidance = discoveredFromUrl ? "" : discovered ? ` Same-origin discovery resolved the same URL (${discovered}), which still did not answer successfully.` : ` No same-origin discovery document was found at /.well-known/haven-demo-merchant or /. If ${merchantUrl} is a base merchant URL, pass the exact MCP endpoint instead (often <origin>/mcp).`;
|
|
551
|
+
return new HavenApiError(`${base}${guidance}`, response.status || 400);
|
|
552
|
+
}
|
|
553
|
+
function normalizeError(err) {
|
|
554
|
+
if (err instanceof HavenPaymentStateError) {
|
|
555
|
+
return {
|
|
556
|
+
success: false,
|
|
557
|
+
code: err.code,
|
|
558
|
+
message: err.message,
|
|
559
|
+
statusCode: err.statusCode,
|
|
560
|
+
paymentId: err.paymentId,
|
|
561
|
+
status: err.status,
|
|
562
|
+
phase: err.phase,
|
|
563
|
+
nextAction: err.nextAction,
|
|
564
|
+
resume_state: err.resumeState,
|
|
565
|
+
body: err.body
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
if (err instanceof HavenSigningError) {
|
|
569
|
+
return {
|
|
570
|
+
success: false,
|
|
571
|
+
code: err.code,
|
|
572
|
+
message: err.message
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (err instanceof HavenApiError) {
|
|
576
|
+
const body = err.body;
|
|
577
|
+
return {
|
|
578
|
+
success: false,
|
|
579
|
+
code: err.code,
|
|
580
|
+
message: err.message,
|
|
581
|
+
statusCode: err.statusCode,
|
|
582
|
+
paymentId: err.paymentId,
|
|
583
|
+
phase: stringOrUndefined(body?.phase),
|
|
584
|
+
nextAction: stringOrUndefined(body?.nextAction) ?? stringOrUndefined(body?.next_action) ?? AgentPaymentNextAction.StopAndTellUser,
|
|
585
|
+
body: err.body
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
if (err instanceof HavenError) {
|
|
589
|
+
return {
|
|
590
|
+
success: false,
|
|
591
|
+
code: err.code,
|
|
592
|
+
message: err.message,
|
|
593
|
+
statusCode: err.statusCode,
|
|
594
|
+
paymentId: err.paymentId
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
success: false,
|
|
599
|
+
code: "UNKNOWN_ERROR",
|
|
600
|
+
message: err instanceof Error ? err.message : String(err),
|
|
601
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function stringOrUndefined(value) {
|
|
605
|
+
return typeof value === "string" ? value : void 0;
|
|
606
|
+
}
|
|
607
|
+
function computeConsentHash(input) {
|
|
608
|
+
const allowanceCanonical = [...input.allowanceSummary].map((a) => `${a.token}:${a.amount}:${a.resetMinutes ?? "none"}`).sort().join("|");
|
|
609
|
+
const toolCanonical = [...input.toolNames].sort().join(",");
|
|
610
|
+
const identity = [
|
|
611
|
+
input.apiKeyPrefix,
|
|
612
|
+
input.apiUrl ?? "",
|
|
613
|
+
input.agentId ?? "",
|
|
614
|
+
(input.safeAddress ?? "").toLowerCase(),
|
|
615
|
+
(input.delegateAddress ?? "").toLowerCase(),
|
|
616
|
+
input.chainId ?? ""
|
|
617
|
+
].join("|");
|
|
618
|
+
return createHash("sha256").update(`${identity}
|
|
619
|
+
${toolCanonical}
|
|
620
|
+
${allowanceCanonical}`).digest("hex").slice(0, 16);
|
|
621
|
+
}
|
|
622
|
+
function renderConsentBlock(input, hash) {
|
|
623
|
+
const lines = [
|
|
624
|
+
"",
|
|
625
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
626
|
+
"Haven MCP server \u2014 first-launch consent",
|
|
627
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
628
|
+
"",
|
|
629
|
+
`Credential: ${input.apiKeyPrefix}\u2026`
|
|
630
|
+
];
|
|
631
|
+
if (input.apiUrl) lines.push(`Haven API: ${input.apiUrl}`);
|
|
632
|
+
if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
|
|
633
|
+
if (input.safeAddress) lines.push(`Haven wallet (Safe): ${input.safeAddress}`);
|
|
634
|
+
if (input.delegateAddress) lines.push(`Delegate (local signer): ${input.delegateAddress}`);
|
|
635
|
+
if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
|
|
636
|
+
lines.push("");
|
|
637
|
+
lines.push("Confirm these match the Haven wallet and chain you intend the");
|
|
638
|
+
lines.push("agent runtime to use. The delegate above is the only key that");
|
|
639
|
+
lines.push("signs payments \u2014 it lives in this process, not on Haven's backend.");
|
|
640
|
+
lines.push("");
|
|
641
|
+
lines.push("Tools this server will expose to your agent runtime:");
|
|
642
|
+
for (const name of input.toolNames) {
|
|
643
|
+
lines.push(` \u2022 ${name}`);
|
|
644
|
+
lines.push(` ${toolDescriptions[name]}`);
|
|
645
|
+
}
|
|
646
|
+
lines.push("");
|
|
647
|
+
if (input.allowanceSummary.length === 0) {
|
|
648
|
+
lines.push("On-chain budget: none configured.");
|
|
649
|
+
lines.push(" Until the wallet owner grants this agent a budget in Haven,");
|
|
650
|
+
lines.push(" every payment it attempts is declined on-chain.");
|
|
651
|
+
} else {
|
|
652
|
+
lines.push("On-chain budget (the real spend gate \u2014 enforced by the agent's");
|
|
653
|
+
lines.push("signed delegation, not by Haven):");
|
|
654
|
+
for (const a of input.allowanceSummary) {
|
|
655
|
+
const reset = a.resetMinutes ? ` per ${a.resetMinutes} min` : " (no reset)";
|
|
656
|
+
lines.push(` \u2022 up to ${a.amount} ${a.token}${reset}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
lines.push("");
|
|
660
|
+
lines.push("Anything above the on-chain budget is declined before any money");
|
|
661
|
+
lines.push("moves \u2014 it is not queued, and no one is asked to review it. If the");
|
|
662
|
+
lines.push("agent needs more room, the wallet owner grants or raises the budget");
|
|
663
|
+
lines.push("in Haven. Revoking the agent on-chain disables every MCP tool that");
|
|
664
|
+
lines.push("would spend.");
|
|
665
|
+
lines.push("");
|
|
666
|
+
lines.push(`Consent hash: ${hash}`);
|
|
667
|
+
lines.push("");
|
|
668
|
+
lines.push("To acknowledge, EITHER:");
|
|
669
|
+
lines.push(` \u2022 set HAVEN_MCP_ACK=${hash} in this process's environment, OR`);
|
|
670
|
+
lines.push(" \u2022 re-run with --ack to write the acknowledgement next to your");
|
|
671
|
+
lines.push(" credential file (sidecar <credentials>.ack.json).");
|
|
672
|
+
lines.push("");
|
|
673
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
674
|
+
lines.push("");
|
|
675
|
+
return lines.join("\n");
|
|
676
|
+
}
|
|
677
|
+
async function ensureConsent(input, options = {}) {
|
|
678
|
+
const env = options.env ?? process.env;
|
|
679
|
+
const out = options.out ?? process.stderr;
|
|
680
|
+
const hash = computeConsentHash(input);
|
|
681
|
+
if (env.HAVEN_MCP_ACK === "skip") {
|
|
682
|
+
return { ok: true, hash, reason: "env_var_skip" };
|
|
683
|
+
}
|
|
684
|
+
if (typeof env.HAVEN_MCP_ACK === "string" && env.HAVEN_MCP_ACK.length > 0) {
|
|
685
|
+
if (env.HAVEN_MCP_ACK === hash) {
|
|
686
|
+
return { ok: true, hash, reason: "env_var_match" };
|
|
687
|
+
}
|
|
688
|
+
out.write(renderConsentBlock(input, hash));
|
|
689
|
+
out.write(
|
|
690
|
+
`HAVEN_MCP_ACK was set but did not match the current consent hash.
|
|
691
|
+
Expected: ${hash}
|
|
692
|
+
Got: ${env.HAVEN_MCP_ACK}
|
|
693
|
+
Re-acknowledge with the new hash above, or run with --ack.
|
|
694
|
+
|
|
695
|
+
`
|
|
696
|
+
);
|
|
697
|
+
return { ok: false, hash, reason: "env_var_mismatch" };
|
|
698
|
+
}
|
|
699
|
+
const ackPath = sidecarPath(options.credentialsPath);
|
|
700
|
+
if (ackPath) {
|
|
701
|
+
const stored = await readAckFile(ackPath);
|
|
702
|
+
if (stored?.ack === hash) {
|
|
703
|
+
return { ok: true, hash, reason: "ack_file_match" };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (options.writeAck && ackPath) {
|
|
707
|
+
out.write(renderConsentBlock(input, hash));
|
|
708
|
+
await writeAckFile(ackPath, hash);
|
|
709
|
+
out.write(`Wrote acknowledgement to ${ackPath}
|
|
710
|
+
|
|
711
|
+
`);
|
|
712
|
+
return { ok: true, hash, reason: "wrote_ack_file" };
|
|
713
|
+
}
|
|
714
|
+
out.write(renderConsentBlock(input, hash));
|
|
715
|
+
return { ok: false, hash, reason: "no_acknowledgement" };
|
|
716
|
+
}
|
|
717
|
+
function sidecarPath(credentialsPath) {
|
|
718
|
+
if (!credentialsPath) return null;
|
|
719
|
+
return resolve(`${credentialsPath}.ack.json`);
|
|
720
|
+
}
|
|
721
|
+
async function readAckFile(path) {
|
|
722
|
+
try {
|
|
723
|
+
const raw = await readFile(path, "utf8");
|
|
724
|
+
const parsed = JSON.parse(raw);
|
|
725
|
+
return { ack: typeof parsed.ack === "string" ? parsed.ack : void 0 };
|
|
726
|
+
} catch {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async function writeAckFile(path, hash) {
|
|
731
|
+
await mkdir(dirname(path), { recursive: true });
|
|
732
|
+
await writeFile(
|
|
733
|
+
path,
|
|
734
|
+
JSON.stringify({ ack: hash, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
735
|
+
"utf8"
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
async function consentInputFromClient(haven, seed, toolNames) {
|
|
739
|
+
let allowanceSummary = seed.allowanceSummary ?? [];
|
|
740
|
+
let safeAddress = seed.safeAddress;
|
|
741
|
+
let delegateAddress = seed.delegateAddress;
|
|
742
|
+
let chainId = seed.chainId;
|
|
743
|
+
try {
|
|
744
|
+
const summary = await haven.getAllowances();
|
|
745
|
+
const list = isAllowanceSummary(summary) ? summary.allowances : Array.isArray(summary) ? summary : [];
|
|
746
|
+
if (isAllowanceSummary(summary)) {
|
|
747
|
+
safeAddress = summary.safeAddress ?? safeAddress;
|
|
748
|
+
delegateAddress = summary.delegateAddress;
|
|
749
|
+
chainId = typeof summary.chainId === "number" ? summary.chainId : chainId;
|
|
750
|
+
}
|
|
751
|
+
const liveAllowanceSummary = list.map((a) => ({
|
|
752
|
+
token: a.tokenSymbol ?? "UNKNOWN",
|
|
753
|
+
amount: a.onchain?.amount ?? a.configuredAmount ?? "0",
|
|
754
|
+
resetMinutes: typeof a.onchain?.resetTimeMin === "number" ? a.onchain.resetTimeMin : typeof a.resetPeriodMin === "number" ? a.resetPeriodMin : null
|
|
755
|
+
}));
|
|
756
|
+
allowanceSummary = liveAllowanceSummary;
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
apiKeyPrefix: derivePrefix(seed.apiKey),
|
|
761
|
+
apiUrl: seed.apiUrl,
|
|
762
|
+
agentId: seed.agentId,
|
|
763
|
+
safeAddress,
|
|
764
|
+
delegateAddress,
|
|
765
|
+
chainId,
|
|
766
|
+
toolNames,
|
|
767
|
+
allowanceSummary
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function isAllowanceSummary(value) {
|
|
771
|
+
return typeof value === "object" && value !== null && "allowances" in value && Array.isArray(value.allowances);
|
|
772
|
+
}
|
|
773
|
+
function derivePrefix(apiKey) {
|
|
774
|
+
return apiKey.slice(0, 12);
|
|
775
|
+
}
|
|
776
|
+
function registeredToolNames() {
|
|
777
|
+
return Object.keys(toolSchemas);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/server.ts
|
|
781
|
+
async function resolveHavenClient(options = {}) {
|
|
782
|
+
assertSupportedNodeVersion(options.nodeVersion);
|
|
783
|
+
const credentialSource = options.credentialsPath || options.identityPath || options.signerPath ? {
|
|
784
|
+
credentialsPath: options.credentialsPath,
|
|
785
|
+
identityPath: options.identityPath,
|
|
786
|
+
signerPath: options.signerPath
|
|
787
|
+
} : void 0;
|
|
788
|
+
const credentials = options.credentials ?? await loadCredentials(credentialSource);
|
|
789
|
+
const client = new HavenClient({
|
|
790
|
+
apiKey: credentials.apiKey,
|
|
791
|
+
delegateKey: credentials.delegateKey,
|
|
792
|
+
baseUrl: credentials.apiUrl
|
|
793
|
+
});
|
|
794
|
+
return { client, credentials };
|
|
795
|
+
}
|
|
796
|
+
var MCP_NAME = "@haven_ai/mcp";
|
|
797
|
+
var MCP_VERSION = "0.0.0-dev.202609031523.fd49e1a";
|
|
798
|
+
var MCP_INSTRUCTIONS = [
|
|
799
|
+
"Haven local MCP server: signs in-process with the delegate key it holds on",
|
|
800
|
+
"this machine \u2014 the key never leaves this process. Call haven_get_agent",
|
|
801
|
+
"first, every session: identity, readiness, and live remaining budget in",
|
|
802
|
+
"one call. Do not guess the wallet, network, or budget.",
|
|
803
|
+
"",
|
|
804
|
+
"To pay: haven_discover_tools to find a payable service, then",
|
|
805
|
+
"haven_pay_mcp_tool for MCP merchants or haven_pay_x402 for any x402",
|
|
806
|
+
"paywall. Tool responses carry nextAction \u2014 follow it; description prose",
|
|
807
|
+
"is fallback, not the source of truth.",
|
|
808
|
+
"",
|
|
809
|
+
"A payment outside the agent budget is declined before any money moves. Haven",
|
|
810
|
+
"holds no approval queue, so there is nothing to poll and no approval will",
|
|
811
|
+
"ever arrive for it. On a decline, or on any status you do not",
|
|
812
|
+
"recognise, stop and tell the user: do not retry or re-sign, and ask them to",
|
|
813
|
+
"grant or raise the budget in Haven. Spend authority is enforced on-chain by",
|
|
814
|
+
"the user's account; Haven never holds keys and cannot override it."
|
|
815
|
+
].join("\n");
|
|
816
|
+
function buildMcpServer(haven) {
|
|
817
|
+
const server = new McpServer(
|
|
818
|
+
{
|
|
819
|
+
name: MCP_NAME,
|
|
820
|
+
version: MCP_VERSION
|
|
821
|
+
},
|
|
822
|
+
{ instructions: MCP_INSTRUCTIONS }
|
|
823
|
+
);
|
|
824
|
+
const handlers = createToolHandlers(haven);
|
|
825
|
+
const registerTool = server.tool.bind(server);
|
|
826
|
+
for (const name of Object.keys(toolSchemas)) {
|
|
827
|
+
registerTool(
|
|
828
|
+
name,
|
|
829
|
+
toolDescriptions[name],
|
|
830
|
+
toolSchemas[name],
|
|
831
|
+
async (args) => haven.withRequestContext(
|
|
832
|
+
{ "X-Haven-MCP-Tool": name },
|
|
833
|
+
async () => toMcpResult(await handlers[name](args))
|
|
834
|
+
)
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
return server;
|
|
838
|
+
}
|
|
839
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node) {
|
|
840
|
+
if (isSupportedNodeVersion(nodeVersion)) return;
|
|
841
|
+
const err = new Error(
|
|
842
|
+
unsupportedNodeVersionMessage({ subject: "The Haven MCP server", nodeVersion })
|
|
843
|
+
);
|
|
844
|
+
err.code = "HAVEN_MCP_UNSUPPORTED_NODE";
|
|
845
|
+
throw err;
|
|
846
|
+
}
|
|
847
|
+
async function runStdioServer(options = {}) {
|
|
848
|
+
const { client: haven, credentials } = await resolveHavenClient(options);
|
|
849
|
+
if (!options.skipConsent) {
|
|
850
|
+
const decision = await runConsentGate(haven, credentials, options);
|
|
851
|
+
if (!decision.ok) {
|
|
852
|
+
const err = new Error(
|
|
853
|
+
decision.reason === "env_var_mismatch" ? "Haven MCP consent acknowledgement does not match the current configuration." : "Haven MCP server requires a one-time consent acknowledgement before starting."
|
|
854
|
+
);
|
|
855
|
+
err.code = "HAVEN_MCP_NO_CONSENT";
|
|
856
|
+
throw err;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const server = buildMcpServer(haven);
|
|
860
|
+
await server.connect(new StdioServerTransport());
|
|
861
|
+
}
|
|
862
|
+
async function runConsentGate(haven, credentials, options) {
|
|
863
|
+
const toolNames = registeredToolNames();
|
|
864
|
+
const input = await consentInputFromClient(
|
|
865
|
+
haven,
|
|
866
|
+
{
|
|
867
|
+
apiKey: credentials.apiKey,
|
|
868
|
+
apiUrl: credentials.apiUrl,
|
|
869
|
+
agentId: credentials.agentId,
|
|
870
|
+
safeAddress: credentials.safeAddress,
|
|
871
|
+
delegateAddress: credentials.delegateAddress,
|
|
872
|
+
chainId: credentials.chainId,
|
|
873
|
+
allowanceSummary: credentials.allowanceSummary
|
|
874
|
+
},
|
|
875
|
+
toolNames
|
|
876
|
+
);
|
|
877
|
+
const credentialsPath = options.identityPath ?? options.credentialsPath ?? credentials.sourcePath;
|
|
878
|
+
return ensureConsent(input, {
|
|
879
|
+
credentialsPath,
|
|
880
|
+
writeAck: options.writeAck
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
function toMcpResult(payload) {
|
|
884
|
+
return {
|
|
885
|
+
isError: !payload.success,
|
|
886
|
+
content: [
|
|
887
|
+
{
|
|
888
|
+
type: "text",
|
|
889
|
+
text: JSON.stringify(payload, null, 2)
|
|
890
|
+
}
|
|
891
|
+
]
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/cli.ts
|
|
896
|
+
function parseArgs(argv) {
|
|
897
|
+
const options = {};
|
|
898
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
899
|
+
const arg = argv[i];
|
|
900
|
+
if (arg === "--credentials" || arg === "--credentials-path") {
|
|
901
|
+
options.credentialsPath = argv[i + 1];
|
|
902
|
+
i += 1;
|
|
903
|
+
} else if (arg === "--identity") {
|
|
904
|
+
options.identityPath = argv[i + 1];
|
|
905
|
+
i += 1;
|
|
906
|
+
} else if (arg === "--signer") {
|
|
907
|
+
options.signerPath = argv[i + 1];
|
|
908
|
+
i += 1;
|
|
909
|
+
} else if (arg === "--transport") {
|
|
910
|
+
const transport = argv[i + 1];
|
|
911
|
+
i += 1;
|
|
912
|
+
if (transport !== "stdio") {
|
|
913
|
+
throw new Error("Only local stdio transport is supported. Haven does not provide a remote MCP signer mode.");
|
|
914
|
+
}
|
|
915
|
+
} else if (arg === "--ack") {
|
|
916
|
+
options.writeAck = true;
|
|
917
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
918
|
+
process.stdout.write([
|
|
919
|
+
"Haven MCP server",
|
|
920
|
+
"",
|
|
921
|
+
"Usage:",
|
|
922
|
+
" npx @haven_ai/mcp --credentials /path/to/agent.json",
|
|
923
|
+
" npx @haven_ai/mcp --identity /path/to/identity.json --signer /path/to/signer.json",
|
|
924
|
+
"",
|
|
925
|
+
"Options:",
|
|
926
|
+
" --credentials <path> Haven credential JSON file. Also supported: HAVEN_CREDENTIALS.",
|
|
927
|
+
" --identity <path> Haven identity JSON file written by @haven_ai/connect.",
|
|
928
|
+
" --signer <path> Haven signer JSON file written by @haven_ai/connect.",
|
|
929
|
+
" --transport stdio Local stdio transport. This is the only supported mode.",
|
|
930
|
+
" --ack Acknowledge the first-launch consent block and write",
|
|
931
|
+
" a sidecar acknowledgement file next to the credential.",
|
|
932
|
+
"",
|
|
933
|
+
"Consent:",
|
|
934
|
+
" On first launch the server prints the tool list and the on-chain",
|
|
935
|
+
" allowance summary, then refuses to start unless you have acknowledged.",
|
|
936
|
+
" Acknowledge with EITHER --ack OR HAVEN_MCP_ACK=<hash> in your environment.",
|
|
937
|
+
""
|
|
938
|
+
].join("\n"));
|
|
939
|
+
process.exit(0);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return options;
|
|
943
|
+
}
|
|
944
|
+
async function main() {
|
|
945
|
+
await runStdioServer(parseArgs(process.argv.slice(2)));
|
|
946
|
+
}
|
|
947
|
+
main().catch((err) => {
|
|
948
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
949
|
+
`);
|
|
950
|
+
process.exit(1);
|
|
951
|
+
});
|
|
952
|
+
//# sourceMappingURL=cli.js.map
|
|
953
|
+
//# sourceMappingURL=cli.js.map
|