@haven_ai/mcp 0.1.0-alpha
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 +154 -0
- package/dist/cli.cjs +589 -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 +587 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +556 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +548 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
5
|
+
var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
6
|
+
var sdk = require('@haven_ai/sdk');
|
|
7
|
+
var promises = require('fs/promises');
|
|
8
|
+
var v3 = require('zod/v3');
|
|
9
|
+
var crypto = require('crypto');
|
|
10
|
+
var path = require('path');
|
|
11
|
+
|
|
12
|
+
async function loadCredentials(path = process.env.HAVEN_CREDENTIALS) {
|
|
13
|
+
if (path) {
|
|
14
|
+
return loadCredentialsFromFile(path);
|
|
15
|
+
}
|
|
16
|
+
const envCreds = loadCredentialsFromEnv();
|
|
17
|
+
if (envCreds) return envCreds;
|
|
18
|
+
throw new Error(
|
|
19
|
+
"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."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
async function loadCredentialsFromFile(path) {
|
|
23
|
+
let rawText;
|
|
24
|
+
try {
|
|
25
|
+
rawText = await promises.readFile(path, "utf8");
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(`Could not read Haven credentials at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
28
|
+
}
|
|
29
|
+
let raw;
|
|
30
|
+
try {
|
|
31
|
+
raw = JSON.parse(rawText);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error("Haven credentials must be JSON with api_key and delegate_key fields.");
|
|
34
|
+
}
|
|
35
|
+
const apiKey = stringField(raw.api_key ?? raw.apiKey);
|
|
36
|
+
const delegateKey = stringField(raw.delegate_key ?? raw.delegateKey);
|
|
37
|
+
if (!apiKey) {
|
|
38
|
+
throw new Error("Haven credentials are missing api_key.");
|
|
39
|
+
}
|
|
40
|
+
if (!delegateKey) {
|
|
41
|
+
throw new Error("Haven MCP requires delegate_key so payments can be signed locally.");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
apiKey,
|
|
45
|
+
delegateKey,
|
|
46
|
+
agentId: stringField(raw.agent_id ?? raw.agentId),
|
|
47
|
+
safeAddress: stringField(raw.safe_address ?? raw.safeAddress),
|
|
48
|
+
apiUrl: stringField(raw.api_url ?? raw.apiUrl),
|
|
49
|
+
sourcePath: path
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function loadCredentialsFromEnv() {
|
|
53
|
+
const apiKey = stringField(process.env.HAVEN_API_KEY);
|
|
54
|
+
const delegateKey = stringField(process.env.HAVEN_DELEGATE_KEY);
|
|
55
|
+
if (!apiKey && !delegateKey) return null;
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
throw new Error("HAVEN_DELEGATE_KEY is set but HAVEN_API_KEY is missing.");
|
|
58
|
+
}
|
|
59
|
+
if (!delegateKey) {
|
|
60
|
+
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.");
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
apiKey,
|
|
64
|
+
delegateKey,
|
|
65
|
+
agentId: stringField(process.env.HAVEN_AGENT_ID),
|
|
66
|
+
safeAddress: stringField(process.env.HAVEN_SAFE_ADDRESS),
|
|
67
|
+
apiUrl: stringField(process.env.HAVEN_API_URL)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function stringField(value) {
|
|
71
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
72
|
+
}
|
|
73
|
+
var headersSchema = v3.z.record(v3.z.string(), v3.z.string()).optional();
|
|
74
|
+
var toolSchemas = {
|
|
75
|
+
haven_quote_x402: {
|
|
76
|
+
url: v3.z.string().url(),
|
|
77
|
+
method: v3.z.string().optional(),
|
|
78
|
+
headers: headersSchema,
|
|
79
|
+
body: v3.z.string().optional(),
|
|
80
|
+
idempotencyKey: v3.z.string().optional()
|
|
81
|
+
},
|
|
82
|
+
haven_pay_x402_quote: {
|
|
83
|
+
quote: v3.z.unknown(),
|
|
84
|
+
idempotencyKey: v3.z.string().optional()
|
|
85
|
+
},
|
|
86
|
+
haven_resume_x402_payment: {
|
|
87
|
+
payment_id: v3.z.string().optional(),
|
|
88
|
+
resume_state: v3.z.unknown().optional()
|
|
89
|
+
},
|
|
90
|
+
haven_quote_mpp: {
|
|
91
|
+
url: v3.z.string().url().optional(),
|
|
92
|
+
challenge: v3.z.unknown().optional(),
|
|
93
|
+
method: v3.z.string().optional(),
|
|
94
|
+
headers: headersSchema,
|
|
95
|
+
body: v3.z.string().optional(),
|
|
96
|
+
idempotencyKey: v3.z.string().optional()
|
|
97
|
+
},
|
|
98
|
+
haven_pay_mpp_challenge: {
|
|
99
|
+
quote: v3.z.unknown(),
|
|
100
|
+
idempotencyKey: v3.z.string().optional()
|
|
101
|
+
},
|
|
102
|
+
haven_resume_mpp_payment: {
|
|
103
|
+
payment_id: v3.z.string().optional(),
|
|
104
|
+
resume_state: v3.z.unknown().optional()
|
|
105
|
+
},
|
|
106
|
+
haven_get_payment_status: {
|
|
107
|
+
payment_id: v3.z.string()
|
|
108
|
+
},
|
|
109
|
+
haven_get_resume_state: {
|
|
110
|
+
payment_id: v3.z.string()
|
|
111
|
+
},
|
|
112
|
+
haven_get_agent: {},
|
|
113
|
+
haven_get_allowances: {},
|
|
114
|
+
haven_list_receipts: {
|
|
115
|
+
limit: v3.z.number().int().min(1).max(100).optional()
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var toolDescriptions = {
|
|
119
|
+
haven_quote_x402: "Inspect an HTTP 402 x402 paid resource without creating a Haven payment, signature, approval, or on-chain transaction.",
|
|
120
|
+
haven_pay_x402_quote: "Pay a previously inspected x402 quote. The delegate key signs locally; Haven only validates and relays signed, on-chain-constrained payment transactions. If approval is needed, preserve the returned resume_state and wait for nextAction=retry_original_x402_request before resuming.",
|
|
121
|
+
haven_resume_x402_payment: "Resume an x402 payment after the Haven wallet owner approved the funding step. Accepts either resume_state or payment_id. Only use when get status returns nextAction=retry_original_x402_request; do not start a new merchant session.",
|
|
122
|
+
haven_quote_mpp: "Inspect a Haven MPP challenge or paid MPP URL without creating a Haven payment, signature, approval, or on-chain transaction.",
|
|
123
|
+
haven_pay_mpp_challenge: "Pay a previously inspected MPP challenge. The delegate key signs locally; Haven only validates and relays signed, on-chain-constrained payment transactions. If approval is needed, preserve resume_state or payment_id.",
|
|
124
|
+
haven_resume_mpp_payment: "Resume an MPP payment after the Haven wallet owner approved the funding step. Accepts either resume_state or payment_id and retries the original paid resource.",
|
|
125
|
+
haven_get_payment_status: "Fetch structured Haven payment status, including phase and nextAction taxonomy for agent recovery.",
|
|
126
|
+
haven_get_resume_state: "Rehydrate stored x402/MPP resume_state by payment_id. This returns context only; signing still happens locally when a resume tool is called.",
|
|
127
|
+
haven_get_agent: "Return the authenticated agent identity, Haven wallet, delegate address, chain, and status.",
|
|
128
|
+
haven_get_allowances: "Return configured and on-chain allowance state for the authenticated agent. On-chain allowance is the real spend gate.",
|
|
129
|
+
haven_list_receipts: "List recent machine-payment receipts/evidence for bookkeeping. Proof header values are not returned."
|
|
130
|
+
};
|
|
131
|
+
function createToolHandlers(haven) {
|
|
132
|
+
return {
|
|
133
|
+
haven_quote_x402: async (input) => {
|
|
134
|
+
const args = objectInput("haven_quote_x402", input);
|
|
135
|
+
return runTool(async () => haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey }));
|
|
136
|
+
},
|
|
137
|
+
haven_pay_x402_quote: async (input) => {
|
|
138
|
+
const args = objectInput("haven_pay_x402_quote", input);
|
|
139
|
+
return runTool(async () => {
|
|
140
|
+
const response = await haven.payX402Quote(args.quote, { idempotencyKey: args.idempotencyKey });
|
|
141
|
+
return responsePayload(response);
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
haven_resume_x402_payment: async (input) => {
|
|
145
|
+
const args = objectInput("haven_resume_x402_payment", input);
|
|
146
|
+
return runTool(async () => {
|
|
147
|
+
const state = await resumeState(args, "x402");
|
|
148
|
+
const response = await haven.resumeX402Payment(state);
|
|
149
|
+
return responsePayload(response);
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
haven_quote_mpp: async (input) => {
|
|
153
|
+
const args = objectInput("haven_quote_mpp", input);
|
|
154
|
+
return runTool(async () => {
|
|
155
|
+
if (args.challenge) {
|
|
156
|
+
return haven.quoteMpp(args.challenge, requestInit(args), {
|
|
157
|
+
idempotencyKey: args.idempotencyKey
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (!args.url) {
|
|
161
|
+
throw new sdk.HavenApiError("haven_quote_mpp requires either url or challenge.", 400);
|
|
162
|
+
}
|
|
163
|
+
return haven.quoteMpp(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey });
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
haven_pay_mpp_challenge: async (input) => {
|
|
167
|
+
const args = objectInput("haven_pay_mpp_challenge", input);
|
|
168
|
+
return runTool(async () => {
|
|
169
|
+
const response = await haven.payMppChallenge(args.quote, { idempotencyKey: args.idempotencyKey });
|
|
170
|
+
return responsePayload(response);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
haven_resume_mpp_payment: async (input) => {
|
|
174
|
+
const args = objectInput("haven_resume_mpp_payment", input);
|
|
175
|
+
return runTool(async () => {
|
|
176
|
+
const state = await resumeState(args, "mpp");
|
|
177
|
+
const response = await haven.resumeMppPayment(state);
|
|
178
|
+
return responsePayload(response);
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
haven_get_payment_status: async (input) => {
|
|
182
|
+
const args = objectInput("haven_get_payment_status", input);
|
|
183
|
+
return runTool(async () => haven.getPaymentStatus(args.payment_id));
|
|
184
|
+
},
|
|
185
|
+
haven_get_resume_state: async (input) => {
|
|
186
|
+
const args = objectInput("haven_get_resume_state", input);
|
|
187
|
+
return runTool(async () => haven.getResumeState(args.payment_id));
|
|
188
|
+
},
|
|
189
|
+
haven_get_agent: async () => runTool(async () => haven.getAgent()),
|
|
190
|
+
haven_get_allowances: async () => runTool(async () => haven.getAllowances()),
|
|
191
|
+
haven_list_receipts: async (input) => {
|
|
192
|
+
const args = objectInput("haven_list_receipts", input);
|
|
193
|
+
return runTool(async () => haven.listReceipts({ limit: args.limit }));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
async function resumeState(args, rail) {
|
|
197
|
+
const state = args.resume_state ?? (args.payment_id ? await haven.getResumeState(args.payment_id) : void 0);
|
|
198
|
+
if (!state || typeof state !== "object") {
|
|
199
|
+
throw new sdk.HavenApiError(`haven_resume_${rail}_payment requires resume_state or payment_id.`, 400);
|
|
200
|
+
}
|
|
201
|
+
if (state.rail !== rail) {
|
|
202
|
+
throw new sdk.HavenApiError(`Resume state is not for the ${rail} rail.`, 409, state);
|
|
203
|
+
}
|
|
204
|
+
return state;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function objectInput(name, input) {
|
|
208
|
+
return v3.z.object(toolSchemas[name]).parse(input ?? {});
|
|
209
|
+
}
|
|
210
|
+
function requestInit(input) {
|
|
211
|
+
if (!input.method && !input.headers && input.body === void 0) return void 0;
|
|
212
|
+
return {
|
|
213
|
+
method: input.method,
|
|
214
|
+
headers: input.headers,
|
|
215
|
+
body: input.body
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function runTool(fn) {
|
|
219
|
+
try {
|
|
220
|
+
return { success: true, data: await fn() };
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return normalizeError(err);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function responsePayload(response) {
|
|
226
|
+
const text = await response.text();
|
|
227
|
+
return {
|
|
228
|
+
status: response.status,
|
|
229
|
+
statusText: response.statusText,
|
|
230
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
231
|
+
body: parseMaybeJson(text)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function parseMaybeJson(text) {
|
|
235
|
+
if (!text) return null;
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(text);
|
|
238
|
+
} catch {
|
|
239
|
+
return text;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function normalizeError(err) {
|
|
243
|
+
if (err instanceof sdk.HavenPaymentStateError) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
code: err.code,
|
|
247
|
+
message: err.message,
|
|
248
|
+
statusCode: err.statusCode,
|
|
249
|
+
paymentId: err.paymentId,
|
|
250
|
+
status: err.status,
|
|
251
|
+
phase: err.phase,
|
|
252
|
+
nextAction: err.nextAction,
|
|
253
|
+
resume_state: err.resumeState,
|
|
254
|
+
body: err.body
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (err instanceof sdk.HavenSigningError) {
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
code: err.code,
|
|
261
|
+
message: err.message
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (err instanceof sdk.HavenApiError) {
|
|
265
|
+
const body = err.body;
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
code: err.code,
|
|
269
|
+
message: err.message,
|
|
270
|
+
statusCode: err.statusCode,
|
|
271
|
+
paymentId: err.paymentId,
|
|
272
|
+
phase: stringOrUndefined(body?.phase),
|
|
273
|
+
nextAction: stringOrUndefined(body?.nextAction) ?? stringOrUndefined(body?.next_action) ?? sdk.AgentPaymentNextAction.StopAndTellUser,
|
|
274
|
+
body: err.body
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (err instanceof sdk.HavenError) {
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
code: err.code,
|
|
281
|
+
message: err.message,
|
|
282
|
+
statusCode: err.statusCode,
|
|
283
|
+
paymentId: err.paymentId
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
code: "UNKNOWN_ERROR",
|
|
289
|
+
message: err instanceof Error ? err.message : String(err),
|
|
290
|
+
nextAction: sdk.AgentPaymentNextAction.StopAndTellUser
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function stringOrUndefined(value) {
|
|
294
|
+
return typeof value === "string" ? value : void 0;
|
|
295
|
+
}
|
|
296
|
+
function computeConsentHash(input) {
|
|
297
|
+
const allowanceCanonical = [...input.allowanceSummary].map((a) => `${a.token}:${a.amount}:${a.resetMinutes ?? "none"}`).sort().join("|");
|
|
298
|
+
const toolCanonical = [...input.toolNames].sort().join(",");
|
|
299
|
+
const identity = [
|
|
300
|
+
input.apiKeyPrefix,
|
|
301
|
+
input.apiUrl ?? "",
|
|
302
|
+
input.agentId ?? "",
|
|
303
|
+
(input.safeAddress ?? "").toLowerCase(),
|
|
304
|
+
(input.delegateAddress ?? "").toLowerCase(),
|
|
305
|
+
input.chainId ?? ""
|
|
306
|
+
].join("|");
|
|
307
|
+
return crypto.createHash("sha256").update(`${identity}
|
|
308
|
+
${toolCanonical}
|
|
309
|
+
${allowanceCanonical}`).digest("hex").slice(0, 16);
|
|
310
|
+
}
|
|
311
|
+
function renderConsentBlock(input, hash) {
|
|
312
|
+
const lines = [
|
|
313
|
+
"",
|
|
314
|
+
"\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",
|
|
315
|
+
"Haven MCP server \u2014 first-launch consent",
|
|
316
|
+
"\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",
|
|
317
|
+
"",
|
|
318
|
+
`Credential: ${input.apiKeyPrefix}\u2026`
|
|
319
|
+
];
|
|
320
|
+
if (input.apiUrl) lines.push(`Haven API: ${input.apiUrl}`);
|
|
321
|
+
if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
|
|
322
|
+
if (input.safeAddress) lines.push(`Haven wallet (Safe): ${input.safeAddress}`);
|
|
323
|
+
if (input.delegateAddress) lines.push(`Delegate (local signer): ${input.delegateAddress}`);
|
|
324
|
+
if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
|
|
325
|
+
lines.push("");
|
|
326
|
+
lines.push("Confirm these match the Haven wallet and chain you intend the");
|
|
327
|
+
lines.push("agent runtime to use. The delegate above is the only key that");
|
|
328
|
+
lines.push("signs payments \u2014 it lives in this process, not on Haven's backend.");
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push("Tools this server will expose to your agent runtime:");
|
|
331
|
+
for (const name of input.toolNames) {
|
|
332
|
+
lines.push(` \u2022 ${name}`);
|
|
333
|
+
lines.push(` ${toolDescriptions[name]}`);
|
|
334
|
+
}
|
|
335
|
+
lines.push("");
|
|
336
|
+
if (input.allowanceSummary.length === 0) {
|
|
337
|
+
lines.push("On-chain allowance: none configured.");
|
|
338
|
+
lines.push(" Any payment will queue for manual approval. The on-chain");
|
|
339
|
+
lines.push(" Safe AllowanceModule is the real spend gate.");
|
|
340
|
+
} else {
|
|
341
|
+
lines.push("On-chain allowance (the real spend gate, Safe AllowanceModule):");
|
|
342
|
+
for (const a of input.allowanceSummary) {
|
|
343
|
+
const reset = a.resetMinutes ? ` per ${a.resetMinutes} min` : " (no reset)";
|
|
344
|
+
lines.push(` \u2022 up to ${a.amount} ${a.token}${reset}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
lines.push("");
|
|
348
|
+
lines.push("Anything above the on-chain allowance pauses for owner approval");
|
|
349
|
+
lines.push("in the Haven dashboard. Revoking the agent on-chain disables");
|
|
350
|
+
lines.push("every MCP tool that would spend.");
|
|
351
|
+
lines.push("");
|
|
352
|
+
lines.push(`Consent hash: ${hash}`);
|
|
353
|
+
lines.push("");
|
|
354
|
+
lines.push("To acknowledge, EITHER:");
|
|
355
|
+
lines.push(` \u2022 set HAVEN_MCP_ACK=${hash} in this process's environment, OR`);
|
|
356
|
+
lines.push(" \u2022 re-run with --ack to write the acknowledgement next to your");
|
|
357
|
+
lines.push(" credential file (sidecar <credentials>.ack.json).");
|
|
358
|
+
lines.push("");
|
|
359
|
+
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");
|
|
360
|
+
lines.push("");
|
|
361
|
+
return lines.join("\n");
|
|
362
|
+
}
|
|
363
|
+
async function ensureConsent(input, options = {}) {
|
|
364
|
+
const env = options.env ?? process.env;
|
|
365
|
+
const out = options.out ?? process.stderr;
|
|
366
|
+
const hash = computeConsentHash(input);
|
|
367
|
+
if (env.HAVEN_MCP_ACK === "skip") {
|
|
368
|
+
return { ok: true, hash, reason: "env_var_skip" };
|
|
369
|
+
}
|
|
370
|
+
if (typeof env.HAVEN_MCP_ACK === "string" && env.HAVEN_MCP_ACK.length > 0) {
|
|
371
|
+
if (env.HAVEN_MCP_ACK === hash) {
|
|
372
|
+
return { ok: true, hash, reason: "env_var_match" };
|
|
373
|
+
}
|
|
374
|
+
out.write(renderConsentBlock(input, hash));
|
|
375
|
+
out.write(
|
|
376
|
+
`HAVEN_MCP_ACK was set but did not match the current consent hash.
|
|
377
|
+
Expected: ${hash}
|
|
378
|
+
Got: ${env.HAVEN_MCP_ACK}
|
|
379
|
+
Re-acknowledge with the new hash above, or run with --ack.
|
|
380
|
+
|
|
381
|
+
`
|
|
382
|
+
);
|
|
383
|
+
return { ok: false, hash, reason: "env_var_mismatch" };
|
|
384
|
+
}
|
|
385
|
+
const ackPath = sidecarPath(options.credentialsPath);
|
|
386
|
+
if (ackPath) {
|
|
387
|
+
const stored = await readAckFile(ackPath);
|
|
388
|
+
if (stored?.ack === hash) {
|
|
389
|
+
return { ok: true, hash, reason: "ack_file_match" };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (options.writeAck && ackPath) {
|
|
393
|
+
out.write(renderConsentBlock(input, hash));
|
|
394
|
+
await writeAckFile(ackPath, hash);
|
|
395
|
+
out.write(`Wrote acknowledgement to ${ackPath}
|
|
396
|
+
|
|
397
|
+
`);
|
|
398
|
+
return { ok: true, hash, reason: "wrote_ack_file" };
|
|
399
|
+
}
|
|
400
|
+
out.write(renderConsentBlock(input, hash));
|
|
401
|
+
return { ok: false, hash, reason: "no_acknowledgement" };
|
|
402
|
+
}
|
|
403
|
+
function sidecarPath(credentialsPath) {
|
|
404
|
+
if (!credentialsPath) return null;
|
|
405
|
+
return path.resolve(`${credentialsPath}.ack.json`);
|
|
406
|
+
}
|
|
407
|
+
async function readAckFile(path) {
|
|
408
|
+
try {
|
|
409
|
+
const raw = await promises.readFile(path, "utf8");
|
|
410
|
+
const parsed = JSON.parse(raw);
|
|
411
|
+
return { ack: typeof parsed.ack === "string" ? parsed.ack : void 0 };
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async function writeAckFile(path$1, hash) {
|
|
417
|
+
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
418
|
+
await promises.writeFile(
|
|
419
|
+
path$1,
|
|
420
|
+
JSON.stringify({ ack: hash, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
421
|
+
"utf8"
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
async function consentInputFromClient(haven, seed, toolNames) {
|
|
425
|
+
let allowanceSummary = [];
|
|
426
|
+
let safeAddress = seed.safeAddress;
|
|
427
|
+
let delegateAddress;
|
|
428
|
+
let chainId;
|
|
429
|
+
try {
|
|
430
|
+
const summary = await haven.getAllowances();
|
|
431
|
+
const list = isAllowanceSummary(summary) ? summary.allowances : Array.isArray(summary) ? summary : [];
|
|
432
|
+
if (isAllowanceSummary(summary)) {
|
|
433
|
+
safeAddress = summary.safeAddress ?? safeAddress;
|
|
434
|
+
delegateAddress = summary.delegateAddress;
|
|
435
|
+
chainId = typeof summary.chainId === "number" ? summary.chainId : chainId;
|
|
436
|
+
}
|
|
437
|
+
allowanceSummary = list.map((a) => ({
|
|
438
|
+
token: a.tokenSymbol ?? "UNKNOWN",
|
|
439
|
+
amount: a.onchain?.amount ?? a.configuredAmount ?? "0",
|
|
440
|
+
resetMinutes: typeof a.onchain?.resetTimeMin === "number" ? a.onchain.resetTimeMin : typeof a.resetPeriodMin === "number" ? a.resetPeriodMin : null
|
|
441
|
+
}));
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
apiKeyPrefix: derivePrefix(seed.apiKey),
|
|
446
|
+
apiUrl: seed.apiUrl,
|
|
447
|
+
agentId: seed.agentId,
|
|
448
|
+
safeAddress,
|
|
449
|
+
delegateAddress,
|
|
450
|
+
chainId,
|
|
451
|
+
toolNames,
|
|
452
|
+
allowanceSummary
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function isAllowanceSummary(value) {
|
|
456
|
+
return typeof value === "object" && value !== null && "allowances" in value && Array.isArray(value.allowances);
|
|
457
|
+
}
|
|
458
|
+
function derivePrefix(apiKey) {
|
|
459
|
+
return apiKey.slice(0, 12);
|
|
460
|
+
}
|
|
461
|
+
function registeredToolNames() {
|
|
462
|
+
return Object.keys(toolSchemas);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/server.ts
|
|
466
|
+
async function resolveHavenClient(options = {}) {
|
|
467
|
+
const credentials = options.credentials ?? await loadCredentials(options.credentialsPath);
|
|
468
|
+
const client = new sdk.HavenClient({
|
|
469
|
+
apiKey: credentials.apiKey,
|
|
470
|
+
delegateKey: credentials.delegateKey,
|
|
471
|
+
baseUrl: credentials.apiUrl
|
|
472
|
+
});
|
|
473
|
+
return { client, credentials };
|
|
474
|
+
}
|
|
475
|
+
function buildMcpServer(haven) {
|
|
476
|
+
const server = new mcp_js.McpServer({
|
|
477
|
+
name: "@haven_ai/mcp",
|
|
478
|
+
version: "0.1.0-alpha"
|
|
479
|
+
});
|
|
480
|
+
const handlers = createToolHandlers(haven);
|
|
481
|
+
const registerTool = server.tool.bind(server);
|
|
482
|
+
for (const name of Object.keys(toolSchemas)) {
|
|
483
|
+
registerTool(
|
|
484
|
+
name,
|
|
485
|
+
toolDescriptions[name],
|
|
486
|
+
toolSchemas[name],
|
|
487
|
+
async (args) => haven.withRequestContext(
|
|
488
|
+
{ "X-Haven-MCP-Tool": name },
|
|
489
|
+
async () => toMcpResult(await handlers[name](args))
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
return server;
|
|
494
|
+
}
|
|
495
|
+
async function runStdioServer(options = {}) {
|
|
496
|
+
const { client: haven, credentials } = await resolveHavenClient(options);
|
|
497
|
+
if (!options.skipConsent) {
|
|
498
|
+
const decision = await runConsentGate(haven, credentials, options);
|
|
499
|
+
if (!decision.ok) {
|
|
500
|
+
const err = new Error(
|
|
501
|
+
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."
|
|
502
|
+
);
|
|
503
|
+
err.code = "HAVEN_MCP_NO_CONSENT";
|
|
504
|
+
throw err;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const server = buildMcpServer(haven);
|
|
508
|
+
await server.connect(new stdio_js.StdioServerTransport());
|
|
509
|
+
}
|
|
510
|
+
async function runConsentGate(haven, credentials, options) {
|
|
511
|
+
const toolNames = registeredToolNames();
|
|
512
|
+
const input = await consentInputFromClient(
|
|
513
|
+
haven,
|
|
514
|
+
{
|
|
515
|
+
apiKey: credentials.apiKey,
|
|
516
|
+
apiUrl: credentials.apiUrl,
|
|
517
|
+
agentId: credentials.agentId,
|
|
518
|
+
safeAddress: credentials.safeAddress
|
|
519
|
+
},
|
|
520
|
+
toolNames
|
|
521
|
+
);
|
|
522
|
+
const credentialsPath = options.credentialsPath ?? credentials.sourcePath;
|
|
523
|
+
return ensureConsent(input, {
|
|
524
|
+
credentialsPath,
|
|
525
|
+
writeAck: options.writeAck
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function toMcpResult(payload) {
|
|
529
|
+
return {
|
|
530
|
+
isError: !payload.success,
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "text",
|
|
534
|
+
text: JSON.stringify(payload, null, 2)
|
|
535
|
+
}
|
|
536
|
+
]
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/cli.ts
|
|
541
|
+
function parseArgs(argv) {
|
|
542
|
+
const options = {};
|
|
543
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
544
|
+
const arg = argv[i];
|
|
545
|
+
if (arg === "--credentials" || arg === "--credentials-path") {
|
|
546
|
+
options.credentialsPath = argv[i + 1];
|
|
547
|
+
i += 1;
|
|
548
|
+
} else if (arg === "--transport") {
|
|
549
|
+
const transport = argv[i + 1];
|
|
550
|
+
i += 1;
|
|
551
|
+
if (transport !== "stdio") {
|
|
552
|
+
throw new Error("Only local stdio transport is supported. Haven does not provide a remote MCP signer mode.");
|
|
553
|
+
}
|
|
554
|
+
} else if (arg === "--ack") {
|
|
555
|
+
options.writeAck = true;
|
|
556
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
557
|
+
process.stdout.write([
|
|
558
|
+
"Haven MCP server",
|
|
559
|
+
"",
|
|
560
|
+
"Usage:",
|
|
561
|
+
" npx @haven_ai/mcp --credentials /path/to/agent.json",
|
|
562
|
+
"",
|
|
563
|
+
"Options:",
|
|
564
|
+
" --credentials <path> Haven credential JSON file. Also supported: HAVEN_CREDENTIALS.",
|
|
565
|
+
" --transport stdio Local stdio transport. This is the only supported mode.",
|
|
566
|
+
" --ack Acknowledge the first-launch consent block and write",
|
|
567
|
+
" a sidecar acknowledgement file next to the credential.",
|
|
568
|
+
"",
|
|
569
|
+
"Consent:",
|
|
570
|
+
" On first launch the server prints the tool list and the on-chain",
|
|
571
|
+
" allowance summary, then refuses to start unless you have acknowledged.",
|
|
572
|
+
" Acknowledge with EITHER --ack OR HAVEN_MCP_ACK=<hash> in your environment.",
|
|
573
|
+
""
|
|
574
|
+
].join("\n"));
|
|
575
|
+
process.exit(0);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return options;
|
|
579
|
+
}
|
|
580
|
+
async function main() {
|
|
581
|
+
await runStdioServer(parseArgs(process.argv.slice(2)));
|
|
582
|
+
}
|
|
583
|
+
main().catch((err) => {
|
|
584
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
585
|
+
`);
|
|
586
|
+
process.exit(1);
|
|
587
|
+
});
|
|
588
|
+
//# sourceMappingURL=cli.cjs.map
|
|
589
|
+
//# sourceMappingURL=cli.cjs.map
|