@gwilll/vouch-mcp 0.1.0 → 0.1.1
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/LICENSE +21 -0
- package/dist/index.js +775 -27
- package/package.json +46 -18
- package/dist/client.d.ts +0 -95
- package/dist/client.js +0 -174
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -12
- package/dist/config.js +0 -18
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.js.map +0 -1
- package/dist/server.d.ts +0 -3
- package/dist/server.js +0 -187
- package/dist/server.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,32 +1,780 @@
|
|
|
1
|
+
// src/index.ts
|
|
1
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
3
|
import { createServer as createHttpServer } from "node:http";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var Schema = z.object({
|
|
8
|
+
VOUCH_API_URL: z.string().url().default("https://vouch.dev"),
|
|
9
|
+
VOUCH_API_KEY: z.string().optional(),
|
|
10
|
+
VOUCH_AGENT_PRIVATE_KEY: z.string().regex(/^0x[0-9a-fA-F]{64}$/).optional(),
|
|
11
|
+
VOUCH_DEFAULT_CHAIN: z.coerce.number().default(4217),
|
|
12
|
+
VOUCH_MAX_PAYMENT: z.string().default("100"),
|
|
13
|
+
// display units; the payer's own cap per fund call
|
|
14
|
+
VOUCH_HTTP_PORT: z.coerce.number().optional()
|
|
15
|
+
});
|
|
16
|
+
function loadConfig(env = process.env) {
|
|
17
|
+
const r = Schema.safeParse(env);
|
|
18
|
+
if (!r.success) {
|
|
19
|
+
const issues = r.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
20
|
+
throw new Error(`Invalid Vouch MCP configuration: ${issues}`);
|
|
21
|
+
}
|
|
22
|
+
return r.data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/server.ts
|
|
26
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
|
+
import { z as z4 } from "zod";
|
|
28
|
+
|
|
29
|
+
// ../shared/dist/chains.js
|
|
30
|
+
var TEMPO_MAINNET_ID = 4217;
|
|
31
|
+
var TEMPO_MODERATO_ID = 42431;
|
|
32
|
+
var BASE_MAINNET_ID = 8453;
|
|
33
|
+
var BASE_SEPOLIA_ID = 84532;
|
|
34
|
+
var TOKENS = {
|
|
35
|
+
[TEMPO_MAINNET_ID]: [
|
|
36
|
+
{ address: "0x20C0000000000000000000000000000000000000", symbol: "pathUSD", name: "pathUSD", decimals: 6, eip3009: false, tip20: true },
|
|
37
|
+
{ address: "0x20C000000000000000000000b9537d11c60E8b50", symbol: "USDC.e", name: "Bridged USDC", decimals: 6, eip3009: false, tip20: true }
|
|
38
|
+
],
|
|
39
|
+
[TEMPO_MODERATO_ID]: [
|
|
40
|
+
{ address: "0x20C0000000000000000000000000000000000000", symbol: "pathUSD", name: "pathUSD (testnet)", decimals: 6, eip3009: false, tip20: true },
|
|
41
|
+
{ address: "0x20C000000000000000000000b9537d11c60E8b50", symbol: "USDC.e", name: "Bridged USDC (testnet)", decimals: 6, eip3009: false, tip20: true }
|
|
42
|
+
],
|
|
43
|
+
[BASE_MAINNET_ID]: [
|
|
44
|
+
{ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", symbol: "USDC", name: "USD Coin", decimals: 6, eip3009: true, tip20: false }
|
|
45
|
+
],
|
|
46
|
+
[BASE_SEPOLIA_ID]: [
|
|
47
|
+
{ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", symbol: "USDC", name: "USD Coin (testnet)", decimals: 6, eip3009: true, tip20: false }
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
var CHAINS = {
|
|
51
|
+
[TEMPO_MAINNET_ID]: { id: TEMPO_MAINNET_ID, name: "Tempo", shortName: "Tempo", rpcUrl: "https://rpc.tempo.xyz", explorer: "https://explore.mainnet.tempo.xyz", testnet: false, feeSponsorship: true },
|
|
52
|
+
[TEMPO_MODERATO_ID]: { id: TEMPO_MODERATO_ID, name: "Tempo Moderato", shortName: "Tempo testnet", rpcUrl: "https://rpc.moderato.tempo.xyz", explorer: "https://explore.moderato.tempo.xyz", testnet: true, feeSponsorship: true },
|
|
53
|
+
[BASE_MAINNET_ID]: { id: BASE_MAINNET_ID, name: "Base", shortName: "Base", rpcUrl: "https://mainnet.base.org", explorer: "https://basescan.org", testnet: false, feeSponsorship: false },
|
|
54
|
+
[BASE_SEPOLIA_ID]: { id: BASE_SEPOLIA_ID, name: "Base Sepolia", shortName: "Base testnet", rpcUrl: "https://sepolia.base.org", explorer: "https://sepolia.basescan.org", testnet: true, feeSponsorship: false }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ../shared/dist/policy.js
|
|
58
|
+
import { z as z2 } from "zod";
|
|
59
|
+
var ZERO_EARN_VAULT = "0x0000000000000000000000000000000000000000";
|
|
60
|
+
var AddressField = z2.string().regex(/^0x[0-9a-fA-F]{40}$/, "address expected");
|
|
61
|
+
var PolicySchema = z2.object({
|
|
62
|
+
autoRelease: z2.union([z2.literal(0), z2.literal(1), z2.literal(2)]),
|
|
63
|
+
minConfidenceBps: z2.number().int().min(0).max(1e4),
|
|
64
|
+
maxAutoAmount: z2.bigint().nonnegative(),
|
|
65
|
+
reviewWindow: z2.number().int().min(0).max(2 ** 32 - 1),
|
|
66
|
+
submitDeadline: z2.number().int().min(0).max(2 ** 32 - 1),
|
|
67
|
+
/** Allow-listed Tempo Earn vault that holds the locked principal ("Earn while locked"); zero address = off. */
|
|
68
|
+
earnVault: AddressField.default(ZERO_EARN_VAULT)
|
|
69
|
+
});
|
|
70
|
+
var PolicyWireSchema = z2.object({
|
|
71
|
+
autoRelease: z2.union([z2.literal(0), z2.literal(1), z2.literal(2)]),
|
|
72
|
+
minConfidenceBps: z2.number().int().min(0).max(1e4),
|
|
73
|
+
maxAutoAmount: z2.string().regex(/^\d+$/),
|
|
74
|
+
reviewWindow: z2.number().int().min(0),
|
|
75
|
+
submitDeadline: z2.number().int().min(0),
|
|
76
|
+
earnVault: AddressField.default(ZERO_EARN_VAULT)
|
|
77
|
+
});
|
|
78
|
+
var POLICY_PRESET_NAMES = ["manual", "trusted", "autopilot", "custom"];
|
|
79
|
+
var DAY = 86400;
|
|
80
|
+
var POLICY_PRESETS = {
|
|
81
|
+
manual: { autoRelease: 0, minConfidenceBps: 0, maxAutoAmount: 0n, reviewWindow: 0, submitDeadline: 14 * DAY, earnVault: ZERO_EARN_VAULT },
|
|
82
|
+
trusted: { autoRelease: 1, minConfidenceBps: 8500, maxAutoAmount: 200000000n, reviewWindow: 3 * DAY, submitDeadline: 14 * DAY, earnVault: ZERO_EARN_VAULT },
|
|
83
|
+
autopilot: { autoRelease: 1, minConfidenceBps: 9e3, maxAutoAmount: 50000000n, reviewWindow: 1 * DAY, submitDeadline: 7 * DAY, earnVault: ZERO_EARN_VAULT }
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ../shared/dist/commit.js
|
|
87
|
+
import { encodeAbiParameters, keccak256, toHex, stringToHex, pad } from "viem";
|
|
88
|
+
var ZERO_BYTES32 = `0x${"0".repeat(64)}`;
|
|
89
|
+
function normaliseText(s) {
|
|
90
|
+
return s.replace(/\r\n?/g, "\n").trim();
|
|
91
|
+
}
|
|
92
|
+
function canonicalJson(value) {
|
|
93
|
+
return JSON.stringify(sortKeys(value));
|
|
94
|
+
}
|
|
95
|
+
function sortKeys(v) {
|
|
96
|
+
if (Array.isArray(v))
|
|
97
|
+
return v.map(sortKeys);
|
|
98
|
+
if (v && typeof v === "object") {
|
|
99
|
+
return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])]));
|
|
100
|
+
}
|
|
101
|
+
return v;
|
|
102
|
+
}
|
|
103
|
+
function deliveryCore(m) {
|
|
104
|
+
return {
|
|
105
|
+
jobId: m.jobId.toLowerCase(),
|
|
106
|
+
submittedBy: m.submittedBy.toLowerCase(),
|
|
107
|
+
files: m.files.map((f) => ({ name: f.name, sha256: f.sha256.toLowerCase(), size: f.size, contentType: f.contentType })),
|
|
108
|
+
links: [...m.links],
|
|
109
|
+
note: normaliseText(m.note)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function hashManifest(m) {
|
|
113
|
+
return keccak256(toHex(canonicalJson(deliveryCore(m))));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ../shared/dist/schemas.js
|
|
117
|
+
import { z as z3 } from "zod";
|
|
118
|
+
var HexSchema = z3.string().regex(/^0x[0-9a-fA-F]*$/, "hex expected");
|
|
119
|
+
var AddressSchema = z3.string().regex(/^0x[0-9a-fA-F]{40}$/, "address expected");
|
|
120
|
+
var Bytes32Schema = z3.string().regex(/^0x[0-9a-fA-F]{64}$/, "bytes32 expected");
|
|
121
|
+
var AmountSchema = z3.string().regex(/^\d+$/, "base-unit amount expected");
|
|
122
|
+
var ChainIdSchema = z3.union([z3.literal(4217), z3.literal(42431), z3.literal(8453), z3.literal(84532)]);
|
|
123
|
+
var CreateJobInputSchema = z3.object({
|
|
124
|
+
title: z3.string().min(3).max(120),
|
|
125
|
+
scopeMd: z3.string().min(10).max(2e4),
|
|
126
|
+
/** Base units (6 decimals), as a string. */
|
|
127
|
+
amount: AmountSchema,
|
|
128
|
+
token: AddressSchema.optional(),
|
|
129
|
+
chainId: ChainIdSchema.optional(),
|
|
130
|
+
/** Email, 0x address, or agent URL. Optional: first submitter becomes the worker. */
|
|
131
|
+
worker: z3.string().max(200).optional(),
|
|
132
|
+
policyPreset: z3.enum(POLICY_PRESET_NAMES).optional(),
|
|
133
|
+
policy: PolicyWireSchema.optional(),
|
|
134
|
+
/** Tempo only: allow-listed Earn vault for "Earn while locked" (GET /earn/vaults). Omit for off. */
|
|
135
|
+
earnVault: AddressSchema.optional(),
|
|
136
|
+
/** Seconds the pay link stays valid before the job is considered abandoned (off-chain only). */
|
|
137
|
+
paymentDeadline: z3.number().int().min(300).max(90 * 86400).optional()
|
|
138
|
+
});
|
|
139
|
+
var FundRouteSchema = z3.object({
|
|
140
|
+
kind: z3.enum(["balance", "batched", "mpp", "x402", "eip3009", "transfer"]),
|
|
141
|
+
label: z3.string(),
|
|
142
|
+
description: z3.string(),
|
|
143
|
+
/** Endpoint or address relevant to the route. */
|
|
144
|
+
target: z3.string(),
|
|
145
|
+
memo: Bytes32Schema.optional()
|
|
146
|
+
});
|
|
147
|
+
var EarnVaultDtoSchema = z3.object({
|
|
148
|
+
address: AddressSchema,
|
|
149
|
+
chainId: ChainIdSchema,
|
|
150
|
+
label: z3.string(),
|
|
151
|
+
asset: AddressSchema,
|
|
152
|
+
assetSymbol: z3.string(),
|
|
153
|
+
venue: z3.string().nullable(),
|
|
154
|
+
engineType: z3.string().nullable(),
|
|
155
|
+
/** Net APY as a decimal string ("0.035"), null when the API has no measurement yet. */
|
|
156
|
+
apy: z3.string().nullable(),
|
|
157
|
+
tvl: z3.string().nullable(),
|
|
158
|
+
verified: z3.boolean(),
|
|
159
|
+
access: z3.string(),
|
|
160
|
+
/** On-chain allow-list state in the Vouch Vault: the only thing that decides whether a job may use it. */
|
|
161
|
+
allowed: z3.boolean()
|
|
162
|
+
});
|
|
163
|
+
var JobDtoSchema = z3.object({
|
|
164
|
+
id: Bytes32Schema,
|
|
165
|
+
shortId: z3.string(),
|
|
166
|
+
chainId: ChainIdSchema,
|
|
167
|
+
title: z3.string(),
|
|
168
|
+
scopeMd: z3.string(),
|
|
169
|
+
scopeHash: Bytes32Schema,
|
|
170
|
+
token: AddressSchema,
|
|
171
|
+
tokenSymbol: z3.string(),
|
|
172
|
+
/** Visible to the payer, the worker, and after settlement; null for the public. */
|
|
173
|
+
amount: AmountSchema.nullable(),
|
|
174
|
+
payer: AddressSchema.nullable(),
|
|
175
|
+
worker: AddressSchema.nullable(),
|
|
176
|
+
workerHint: z3.string().nullable(),
|
|
177
|
+
status: z3.string(),
|
|
178
|
+
pill: z3.string(),
|
|
179
|
+
verdict: z3.enum(["PASS", "NEEDS_REVIEW", "FAIL"]).nullable(),
|
|
180
|
+
confidenceBps: z3.number().nullable(),
|
|
181
|
+
policy: PolicyWireSchema,
|
|
182
|
+
fundedAt: z3.string().nullable(),
|
|
183
|
+
submittedAt: z3.string().nullable(),
|
|
184
|
+
attestedAt: z3.string().nullable(),
|
|
185
|
+
settledAt: z3.string().nullable(),
|
|
186
|
+
submitDeadlineAt: z3.string().nullable(),
|
|
187
|
+
autoSettleAt: z3.string().nullable(),
|
|
188
|
+
resubmits: z3.number(),
|
|
189
|
+
deliverableHash: Bytes32Schema.nullable(),
|
|
190
|
+
attestationHash: Bytes32Schema.nullable(),
|
|
191
|
+
txs: z3.record(z3.string(), z3.string()),
|
|
192
|
+
payUrl: z3.string(),
|
|
193
|
+
fundRoutes: z3.array(FundRouteSchema),
|
|
194
|
+
feeBps: z3.number(),
|
|
195
|
+
createdAt: z3.string(),
|
|
196
|
+
updatedAt: z3.string(),
|
|
197
|
+
role: z3.enum(["payer", "worker", "arbiter", "public"])
|
|
198
|
+
});
|
|
199
|
+
var CreateJobOutputSchema = z3.object({
|
|
200
|
+
jobId: Bytes32Schema,
|
|
201
|
+
shortId: z3.string(),
|
|
202
|
+
commit: Bytes32Schema,
|
|
203
|
+
payUrl: z3.string(),
|
|
204
|
+
mcpHint: z3.string(),
|
|
205
|
+
fundRoutes: z3.array(FundRouteSchema),
|
|
206
|
+
job: JobDtoSchema
|
|
207
|
+
});
|
|
208
|
+
var SubmitFileSchema = z3.object({
|
|
209
|
+
name: z3.string().min(1).max(200),
|
|
210
|
+
contentType: z3.string().max(100),
|
|
211
|
+
/** base64 payload; ≤ 2 MB per artifact after decoding (spec §7.6). */
|
|
212
|
+
base64: z3.string().max(28e5)
|
|
213
|
+
});
|
|
214
|
+
var SubmitInputSchema = z3.object({
|
|
215
|
+
files: z3.array(SubmitFileSchema).max(20).default([]),
|
|
216
|
+
links: z3.array(z3.string().url().max(500)).max(20).default([]),
|
|
217
|
+
note: z3.string().max(5e3).default(""),
|
|
218
|
+
/** Worker address; required when the job has no worker yet and the caller is an API key. */
|
|
219
|
+
worker: AddressSchema.optional(),
|
|
220
|
+
/** Optional pre-signed EIP-712 Submit/Resubmit so the relayer can pay gas. */
|
|
221
|
+
signature: z3.object({ signer: AddressSchema, deadline: z3.string(), signature: HexSchema }).optional()
|
|
222
|
+
});
|
|
223
|
+
var ActionSignatureSchema = z3.object({ signer: AddressSchema, deadline: z3.string(), signature: HexSchema });
|
|
224
|
+
var DisputeInputSchema = z3.object({
|
|
225
|
+
reason: z3.string().min(10).max(5e3),
|
|
226
|
+
signature: ActionSignatureSchema.optional()
|
|
227
|
+
});
|
|
228
|
+
var ApproveInputSchema = z3.object({ signature: ActionSignatureSchema.optional() });
|
|
229
|
+
var ResolveInputSchema = z3.object({
|
|
230
|
+
workerBps: z3.number().int().min(0).max(1e4),
|
|
231
|
+
note: z3.string().max(5e3).default("")
|
|
232
|
+
});
|
|
233
|
+
var ScopeItemStatus = z3.enum(["met", "partial", "missing", "unverifiable"]);
|
|
234
|
+
var VerdictOutputSchema = z3.object({
|
|
235
|
+
verdict: z3.enum(["PASS", "NEEDS_REVIEW", "FAIL"]),
|
|
236
|
+
confidence: z3.number().min(0).max(1),
|
|
237
|
+
scope_items: z3.array(z3.object({
|
|
238
|
+
item: z3.string().min(1).max(500),
|
|
239
|
+
status: ScopeItemStatus,
|
|
240
|
+
evidence: z3.string().max(2e3)
|
|
241
|
+
})).min(1).max(50),
|
|
242
|
+
summary: z3.string().min(1).max(3e3),
|
|
243
|
+
questions_for_worker: z3.array(z3.string().max(500)).max(20).default([]),
|
|
244
|
+
red_flags: z3.array(z3.string().max(500)).max(20).default([])
|
|
245
|
+
});
|
|
246
|
+
var VerdictReportSchema = VerdictOutputSchema.extend({
|
|
247
|
+
version: z3.literal(1),
|
|
248
|
+
jobId: Bytes32Schema,
|
|
249
|
+
deliverableHash: Bytes32Schema,
|
|
250
|
+
scopeHash: Bytes32Schema,
|
|
251
|
+
model: z3.string(),
|
|
252
|
+
promptHash: Bytes32Schema,
|
|
253
|
+
responseHash: Bytes32Schema,
|
|
254
|
+
contentHashes: z3.array(Bytes32Schema),
|
|
255
|
+
resubmission: z3.number().int().min(0),
|
|
256
|
+
createdAt: z3.string(),
|
|
257
|
+
/** Post-rule adjustments applied by the service (caps), for auditability. */
|
|
258
|
+
adjustments: z3.array(z3.string())
|
|
259
|
+
});
|
|
260
|
+
var VerdictDtoSchema = z3.object({
|
|
261
|
+
jobId: Bytes32Schema,
|
|
262
|
+
verdict: z3.enum(["PASS", "NEEDS_REVIEW", "FAIL"]).nullable(),
|
|
263
|
+
confidence: z3.number().nullable(),
|
|
264
|
+
confidenceBps: z3.number().nullable(),
|
|
265
|
+
scope_items: VerdictOutputSchema.shape.scope_items.nullable(),
|
|
266
|
+
summary: z3.string().nullable(),
|
|
267
|
+
questions_for_worker: z3.array(z3.string()),
|
|
268
|
+
red_flags: z3.array(z3.string()),
|
|
269
|
+
attestationTx: z3.string().nullable(),
|
|
270
|
+
attestationHash: Bytes32Schema.nullable(),
|
|
271
|
+
reportUrl: z3.string().nullable(),
|
|
272
|
+
stage: z3.enum(["queued", "reading_scope", "checking_files", "writing_report", "attesting", "done", "failed"]).nullable(),
|
|
273
|
+
autoSettleAt: z3.string().nullable()
|
|
274
|
+
});
|
|
275
|
+
var TimelineEventSchema = z3.object({
|
|
276
|
+
kind: z3.string(),
|
|
277
|
+
at: z3.string(),
|
|
278
|
+
txHash: z3.string().nullable(),
|
|
279
|
+
chainId: ChainIdSchema.nullable(),
|
|
280
|
+
detail: z3.string().nullable(),
|
|
281
|
+
source: z3.enum(["chain", "service"])
|
|
282
|
+
});
|
|
283
|
+
var CreateAgentKeyInputSchema = z3.object({
|
|
284
|
+
label: z3.string().min(1).max(100),
|
|
285
|
+
/** Wallet the key is bound to; actions are signed by this wallet (agent holds the private key). */
|
|
286
|
+
address: AddressSchema,
|
|
287
|
+
/** Signature over `Vouch API key for <address> at <timestamp>` proving control of the wallet. */
|
|
288
|
+
timestamp: z3.number().int(),
|
|
289
|
+
signature: HexSchema
|
|
290
|
+
});
|
|
291
|
+
function agentKeyChallenge(address, timestamp) {
|
|
292
|
+
return `Vouch API key for ${address.toLowerCase()} at ${timestamp}`;
|
|
293
|
+
}
|
|
294
|
+
var ApiErrorSchema = z3.object({
|
|
295
|
+
error: z3.object({
|
|
296
|
+
code: z3.string(),
|
|
297
|
+
message: z3.string(),
|
|
298
|
+
next: z3.string().optional(),
|
|
299
|
+
retryable: z3.boolean().default(false)
|
|
300
|
+
})
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ../shared/dist/format.js
|
|
304
|
+
import { formatUnits, parseUnits } from "viem";
|
|
305
|
+
function formatAmount(base, opts = {}) {
|
|
306
|
+
const v = typeof base === "bigint" ? base : BigInt(base);
|
|
307
|
+
const s = formatUnits(v, 6);
|
|
308
|
+
const n = Number(s);
|
|
309
|
+
const text2 = n.toLocaleString("en-US", { minimumFractionDigits: opts.compact ? 0 : 2, maximumFractionDigits: 2 });
|
|
310
|
+
return opts.symbol ? `${text2} ${opts.symbol}` : `$${text2}`;
|
|
311
|
+
}
|
|
312
|
+
function parseAmount(human) {
|
|
313
|
+
const t = human.trim().replace(/^\$/, "").replace(/,/g, "");
|
|
314
|
+
if (!/^\d+(\.\d{1,6})?$/.test(t))
|
|
315
|
+
throw new Error("Enter an amount with up to 6 decimal places");
|
|
316
|
+
return parseUnits(t, 6);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ../shared/dist/zone.js
|
|
320
|
+
import * as Bytes from "ox/Bytes";
|
|
321
|
+
import * as Hex from "ox/Hex";
|
|
322
|
+
import * as PublicKey from "ox/PublicKey";
|
|
323
|
+
import * as Secp256k1 from "ox/Secp256k1";
|
|
324
|
+
var ZERO_MEMO = `0x${"00".repeat(32)}`;
|
|
325
|
+
|
|
326
|
+
// src/client.ts
|
|
327
|
+
import { createHash } from "node:crypto";
|
|
328
|
+
import { readFile } from "node:fs/promises";
|
|
329
|
+
import { basename, extname } from "node:path";
|
|
330
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
331
|
+
import { Mppx, evm, tempo } from "mppx/client";
|
|
332
|
+
var VouchError = class extends Error {
|
|
333
|
+
constructor(message, code, next) {
|
|
334
|
+
super(message);
|
|
335
|
+
this.code = code;
|
|
336
|
+
this.next = next;
|
|
337
|
+
}
|
|
338
|
+
code;
|
|
339
|
+
next;
|
|
340
|
+
};
|
|
341
|
+
var MIME = {
|
|
342
|
+
".md": "text/markdown",
|
|
343
|
+
".txt": "text/plain",
|
|
344
|
+
".json": "application/json",
|
|
345
|
+
".csv": "text/csv",
|
|
346
|
+
".html": "text/html",
|
|
347
|
+
".pdf": "application/pdf",
|
|
348
|
+
".png": "image/png",
|
|
349
|
+
".jpg": "image/jpeg",
|
|
350
|
+
".jpeg": "image/jpeg",
|
|
351
|
+
".gif": "image/gif",
|
|
352
|
+
".webp": "image/webp",
|
|
353
|
+
".js": "text/javascript",
|
|
354
|
+
".ts": "text/typescript",
|
|
355
|
+
".py": "text/x-python",
|
|
356
|
+
".sol": "text/plain",
|
|
357
|
+
".zip": "application/zip"
|
|
358
|
+
};
|
|
359
|
+
var VouchClient = class {
|
|
360
|
+
constructor(cfg) {
|
|
361
|
+
this.cfg = cfg;
|
|
362
|
+
this.account = cfg.VOUCH_AGENT_PRIVATE_KEY ? privateKeyToAccount(cfg.VOUCH_AGENT_PRIVATE_KEY) : null;
|
|
363
|
+
this.apiKey = cfg.VOUCH_API_KEY;
|
|
364
|
+
}
|
|
365
|
+
cfg;
|
|
366
|
+
account;
|
|
367
|
+
apiKey;
|
|
368
|
+
payer = null;
|
|
369
|
+
get address() {
|
|
370
|
+
return this.account?.address ?? null;
|
|
371
|
+
}
|
|
372
|
+
url(path) {
|
|
373
|
+
return `${this.cfg.VOUCH_API_URL.replace(/\/$/, "")}/api/v1${path}`;
|
|
374
|
+
}
|
|
375
|
+
/** Issue an API key bound to the agent wallet on first use (no login: the wallet is the identity). */
|
|
376
|
+
async ensureApiKey() {
|
|
377
|
+
if (this.apiKey) return this.apiKey;
|
|
378
|
+
if (!this.account) return void 0;
|
|
379
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
380
|
+
const signature = await this.account.signMessage({ message: agentKeyChallenge(this.account.address, timestamp) });
|
|
381
|
+
const res = await fetch(this.url("/agents/keys"), {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: { "content-type": "application/json" },
|
|
384
|
+
body: JSON.stringify({ label: `mcp ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`, address: this.account.address, timestamp, signature })
|
|
385
|
+
});
|
|
386
|
+
const data = await res.json();
|
|
387
|
+
if (!res.ok || !data.key) throw new VouchError(data.error?.message ?? "could not create API key", "key_error");
|
|
388
|
+
this.apiKey = data.key;
|
|
389
|
+
return this.apiKey;
|
|
390
|
+
}
|
|
391
|
+
async request(method, path, body, extraHeaders = {}) {
|
|
392
|
+
const key = await this.ensureApiKey();
|
|
393
|
+
const res = await fetch(this.url(path), {
|
|
394
|
+
method,
|
|
395
|
+
// x-api-key (not Authorization) so the MPP `Authorization: Payment …` credential can coexist on /fund.
|
|
396
|
+
headers: { "content-type": "application/json", ...key ? { "x-api-key": key } : {}, ...extraHeaders },
|
|
397
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
398
|
+
});
|
|
399
|
+
const text2 = await res.text();
|
|
400
|
+
let data = {};
|
|
401
|
+
try {
|
|
402
|
+
data = text2 ? JSON.parse(text2) : {};
|
|
403
|
+
} catch {
|
|
404
|
+
data = { raw: text2 };
|
|
405
|
+
}
|
|
406
|
+
if (!res.ok) {
|
|
407
|
+
const err = data.error;
|
|
408
|
+
throw new VouchError(err?.message ?? `HTTP ${res.status}`, err?.code ?? `http_${res.status}`, err?.next);
|
|
409
|
+
}
|
|
410
|
+
return data;
|
|
411
|
+
}
|
|
412
|
+
// ---- jobs ----
|
|
413
|
+
createJob(input) {
|
|
414
|
+
return this.request("POST", "/jobs", input);
|
|
415
|
+
}
|
|
416
|
+
getJob(id) {
|
|
417
|
+
return this.request("GET", `/jobs/${id}`);
|
|
418
|
+
}
|
|
419
|
+
listJobs(status) {
|
|
420
|
+
return this.request("GET", `/jobs${status ? `?status=${encodeURIComponent(status)}` : ""}`);
|
|
421
|
+
}
|
|
422
|
+
getVerdict(id) {
|
|
423
|
+
return this.request("GET", `/jobs/${id}/verdict`);
|
|
424
|
+
}
|
|
425
|
+
getTimeline(id) {
|
|
426
|
+
return this.request("GET", `/jobs/${id}/timeline`);
|
|
427
|
+
}
|
|
428
|
+
// ---- payments (MPP on Tempo, x402-compatible evm/charge on Base) ----
|
|
429
|
+
payerFetch() {
|
|
430
|
+
if (!this.account) throw new VouchError("VOUCH_AGENT_PRIVATE_KEY is not set.", "no_wallet", "Set VOUCH_AGENT_PRIVATE_KEY to a wallet holding pathUSD (Tempo) or USDC (Base).");
|
|
431
|
+
if (!this.payer) {
|
|
432
|
+
const max = this.cfg.VOUCH_MAX_PAYMENT;
|
|
433
|
+
const mppx = Mppx.create({
|
|
434
|
+
polyfill: false,
|
|
435
|
+
methods: [tempo({ account: this.account }), evm({ account: this.account, maxAmount: max })]
|
|
436
|
+
});
|
|
437
|
+
this.payer = { fetch: mppx.fetch };
|
|
438
|
+
}
|
|
439
|
+
return this.payer.fetch;
|
|
440
|
+
}
|
|
441
|
+
/** POST /fund. A 402 is paid automatically by mppx (Tempo charge or EIP-3009), then the 200 comes back. */
|
|
442
|
+
async fundJob(id) {
|
|
443
|
+
const key = await this.ensureApiKey();
|
|
444
|
+
const doFetch = this.payerFetch();
|
|
445
|
+
const res = await doFetch(this.url(`/jobs/${id}/fund`), {
|
|
446
|
+
method: "POST",
|
|
447
|
+
headers: { "content-type": "application/json", ...key ? { "x-api-key": key } : {} },
|
|
448
|
+
body: "{}"
|
|
449
|
+
});
|
|
450
|
+
const data = await res.json().catch(() => ({}));
|
|
451
|
+
if (res.status === 402) throw new VouchError("Payment required but the wallet could not pay.", "payment_failed", "Check the agent wallet holds enough pathUSD/USDC on the job's chain, or set VOUCH_MAX_PAYMENT higher.");
|
|
452
|
+
if (!res.ok || !data.job) throw new VouchError(data.error?.message ?? `HTTP ${res.status}`, data.error?.code ?? "fund_failed", data.error?.next);
|
|
453
|
+
return { status: data.status ?? data.job.status, route: data.route, tx: data.tx, paymentTx: data.paymentTx, job: data.job, note: data.note };
|
|
454
|
+
}
|
|
455
|
+
// ---- signed actions ----
|
|
456
|
+
async signAction(id, action, params) {
|
|
457
|
+
if (!this.account) throw new VouchError("VOUCH_AGENT_PRIVATE_KEY is not set.", "no_wallet");
|
|
458
|
+
const qs = new URLSearchParams({ action, signer: this.account.address, ...params }).toString();
|
|
459
|
+
const { typedData } = await this.request("GET", `/jobs/${id}/sign?${qs}`);
|
|
460
|
+
const message = Object.fromEntries(Object.entries(typedData.message).map(([k, v]) => [k, k === "nonce" || k === "deadline" ? BigInt(v) : v]));
|
|
461
|
+
const signature = await this.account.signTypedData({
|
|
462
|
+
domain: typedData.domain,
|
|
463
|
+
types: typedData.types,
|
|
464
|
+
primaryType: typedData.primaryType,
|
|
465
|
+
message
|
|
466
|
+
});
|
|
467
|
+
return { signer: this.account.address, deadline: typedData.message.deadline, signature };
|
|
468
|
+
}
|
|
469
|
+
async submitDelivery(id, input, resubmit = false) {
|
|
470
|
+
if (!this.account) throw new VouchError("VOUCH_AGENT_PRIVATE_KEY is not set.", "no_wallet");
|
|
471
|
+
const files = [];
|
|
472
|
+
const payload = [];
|
|
473
|
+
for (const path of input.files) {
|
|
474
|
+
const bytes = await readFile(path);
|
|
475
|
+
const sha256 = `0x${createHash("sha256").update(bytes).digest("hex")}`;
|
|
476
|
+
const contentType = MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
477
|
+
files.push({ name: basename(path), sha256, size: bytes.length, contentType });
|
|
478
|
+
payload.push({ name: basename(path), contentType, base64: bytes.toString("base64") });
|
|
479
|
+
}
|
|
480
|
+
const core = { jobId: id, submittedBy: this.account.address, files, links: input.links, note: input.note };
|
|
481
|
+
const deliverableHash = hashManifest(core);
|
|
482
|
+
const signature = await this.signAction(id, resubmit ? "Resubmit" : "Submit", { deliverableHash });
|
|
483
|
+
return this.request(
|
|
484
|
+
"POST",
|
|
485
|
+
`/jobs/${id}/${resubmit ? "resubmit" : "submit"}`,
|
|
486
|
+
{ files: payload, links: input.links, note: input.note, worker: this.account.address, signature }
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
async approve(id) {
|
|
490
|
+
const signature = await this.signAction(id, "Settle", {});
|
|
491
|
+
return this.request("POST", `/jobs/${id}/approve`, { signature });
|
|
492
|
+
}
|
|
493
|
+
async dispute(id, reason) {
|
|
494
|
+
const signature = await this.signAction(id, "Dispute", { reason });
|
|
495
|
+
return this.request("POST", `/jobs/${id}/dispute`, { reason, signature });
|
|
496
|
+
}
|
|
497
|
+
/** Poll until the verifier is done (or a timeout). */
|
|
498
|
+
async waitForVerdict(id, timeoutMs = 18e4, intervalMs = 5e3) {
|
|
499
|
+
const deadline = Date.now() + timeoutMs;
|
|
500
|
+
let last = null;
|
|
501
|
+
while (Date.now() < deadline) {
|
|
502
|
+
last = await this.getVerdict(id);
|
|
503
|
+
if (last.stage === "done" || last.stage === "failed") return last;
|
|
504
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
505
|
+
}
|
|
506
|
+
return last ?? await this.getVerdict(id);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// src/server.ts
|
|
511
|
+
var text = (v) => ({ content: [{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) }] });
|
|
512
|
+
var fail = (e) => {
|
|
513
|
+
const err = e instanceof VouchError ? e : e instanceof Error ? new VouchError(e.message, "error") : new VouchError(String(e), "error");
|
|
514
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: err.code, message: err.message, next: err.next }, null, 2) }] };
|
|
515
|
+
};
|
|
516
|
+
function summarise(job) {
|
|
517
|
+
return {
|
|
518
|
+
jobId: job.id,
|
|
519
|
+
shortId: job.shortId,
|
|
520
|
+
title: job.title,
|
|
521
|
+
status: job.status,
|
|
522
|
+
pill: job.pill,
|
|
523
|
+
chainId: job.chainId,
|
|
524
|
+
amount: job.amount ? formatAmount(job.amount, { symbol: job.tokenSymbol }) : null,
|
|
525
|
+
verdict: job.verdict,
|
|
526
|
+
confidence: job.confidenceBps != null ? job.confidenceBps / 1e4 : null,
|
|
527
|
+
autoSettleAt: job.autoSettleAt,
|
|
528
|
+
payUrl: job.payUrl,
|
|
529
|
+
txs: job.txs,
|
|
530
|
+
yourRole: job.role
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function createServer(cfg) {
|
|
534
|
+
const client = new VouchClient(cfg);
|
|
535
|
+
const server = new McpServer({ name: "vouch", version: "0.1.1" });
|
|
536
|
+
server.registerTool(
|
|
537
|
+
"vouch_create_job",
|
|
538
|
+
{
|
|
539
|
+
title: "Create a Vouch job",
|
|
540
|
+
description: 'Lock stablecoins against a written scope. Returns a jobId to fund with vouch_fund_job, a pay link for humans, and the fund routes. Amount is in dollars (e.g. "5" or "12.50"). Policy presets: manual (you approve), trusted (auto-pay on PASS \u226585% after 3 days, \u2264$200), autopilot (auto-pay on PASS \u226590% after 1 day, \u2264$50).',
|
|
541
|
+
inputSchema: {
|
|
542
|
+
title: z4.string().min(3).max(120).describe("Short job title"),
|
|
543
|
+
scope: z4.string().min(10).max(2e4).describe("The scope in markdown: deliverables, format, deadline, out of scope. This is the contract the verifier checks against."),
|
|
544
|
+
amount: z4.string().describe('Amount in dollars, e.g. "5" or "12.50"'),
|
|
545
|
+
chainId: z4.number().int().optional().describe("4217 Tempo (default), 42431 Tempo testnet, 8453 Base, 84532 Base Sepolia"),
|
|
546
|
+
token: z4.string().optional().describe("Token address; defaults to pathUSD on Tempo, USDC on Base"),
|
|
547
|
+
worker: z4.string().optional().describe("Worker wallet address, email or agent URL. Leave empty to let the first submitter take the job."),
|
|
548
|
+
policyPreset: z4.enum(POLICY_PRESET_NAMES).optional().describe("manual | trusted | autopilot | custom"),
|
|
549
|
+
policy: z4.object({ autoRelease: z4.number().int().min(0).max(2), minConfidenceBps: z4.number().int().min(0).max(1e4), maxAutoAmount: z4.string(), reviewWindow: z4.number().int(), submitDeadline: z4.number().int() }).optional(),
|
|
550
|
+
earnVault: z4.string().optional().describe("Tempo only: Earn vault address for 'Earn while locked' \u2014 the locked principal earns yield for the payer while the work happens. Use vouch_list_earn_vaults to pick one. Omit for off.")
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
async (args) => {
|
|
554
|
+
try {
|
|
555
|
+
const amount = parseAmount(args.amount).toString();
|
|
556
|
+
const r = await client.createJob({
|
|
557
|
+
title: args.title,
|
|
558
|
+
scopeMd: args.scope,
|
|
559
|
+
amount,
|
|
560
|
+
chainId: args.chainId ?? cfg.VOUCH_DEFAULT_CHAIN,
|
|
561
|
+
token: args.token,
|
|
562
|
+
worker: args.worker,
|
|
563
|
+
policyPreset: args.policyPreset,
|
|
564
|
+
policy: args.policy,
|
|
565
|
+
earnVault: args.earnVault
|
|
24
566
|
});
|
|
25
|
-
|
|
26
|
-
|
|
567
|
+
return text({ jobId: r.jobId, shortId: r.shortId, payUrl: r.payUrl, nextStep: `Call vouch_fund_job with jobId ${r.jobId} to lock the funds.`, fundRoutes: r.fundRoutes, job: summarise(r.job) });
|
|
568
|
+
} catch (e) {
|
|
569
|
+
return fail(e);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
server.registerTool(
|
|
574
|
+
"vouch_fund_job",
|
|
575
|
+
{
|
|
576
|
+
title: "Fund a Vouch job",
|
|
577
|
+
description: "Lock the job's amount in the vault from the agent wallet (VOUCH_AGENT_PRIVATE_KEY). On Tempo this pays an MPP charge (TIP-20 transfer with memo = jobId, fees sponsored); on Base an x402/EIP-3009 authorisation. If the wallet already has a Vouch balance, no payment is taken. Returns the funding transaction.",
|
|
578
|
+
inputSchema: { jobId: z4.string().describe("The job id (0x\u2026 or short id)") }
|
|
579
|
+
},
|
|
580
|
+
async ({ jobId }) => {
|
|
581
|
+
try {
|
|
582
|
+
const r = await client.fundJob(jobId);
|
|
583
|
+
return text({ status: r.status, route: r.route, fundTx: r.tx, paymentTx: r.paymentTx, note: r.note, job: summarise(r.job) });
|
|
584
|
+
} catch (e) {
|
|
585
|
+
return fail(e);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
server.registerTool(
|
|
590
|
+
"vouch_submit_delivery",
|
|
591
|
+
{
|
|
592
|
+
title: "Submit a delivery",
|
|
593
|
+
description: "Deliver work for a funded job as the worker (agent wallet). Files are read from local paths, fingerprinted (sha256) and pinned before review; links and a note are included. Signs the on-chain Submit with the agent wallet. The verifier runs automatically afterwards \u2014 call vouch_get_verdict to read the result.",
|
|
594
|
+
inputSchema: {
|
|
595
|
+
jobId: z4.string(),
|
|
596
|
+
files: z4.array(z4.string()).default([]).describe("Local file paths (\u2264 2 MB each)"),
|
|
597
|
+
links: z4.array(z4.string().url()).default([]).describe("URLs: GitHub repos/PRs, Figma files, documents"),
|
|
598
|
+
note: z4.string().default("").describe("Message to the payer / verifier"),
|
|
599
|
+
resubmit: z4.boolean().default(false).describe("true when resubmitting after a FAIL verdict")
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
async (args) => {
|
|
603
|
+
try {
|
|
604
|
+
const r = await client.submitDelivery(args.jobId, { files: args.files, links: args.links, note: args.note }, args.resubmit);
|
|
605
|
+
return text({ status: r.status, tx: r.tx, deliverableHash: r.deliverableHash, verifier: r.verifier, nextStep: "Call vouch_get_verdict (it polls until the verifier finishes).", job: summarise(r.job) });
|
|
606
|
+
} catch (e) {
|
|
607
|
+
return fail(e);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
server.registerTool(
|
|
612
|
+
"vouch_get_verdict",
|
|
613
|
+
{
|
|
614
|
+
title: "Get the verification verdict",
|
|
615
|
+
description: "Verdict (PASS | NEEDS_REVIEW | FAIL), confidence, per-scope-item checklist with evidence, questions for the worker, red flags, attestation tx and full report URL. Waits up to `waitSeconds` for the verifier to finish.",
|
|
616
|
+
inputSchema: { jobId: z4.string(), waitSeconds: z4.number().int().min(0).max(600).default(120) }
|
|
617
|
+
},
|
|
618
|
+
async ({ jobId, waitSeconds }) => {
|
|
619
|
+
try {
|
|
620
|
+
const v = waitSeconds > 0 ? await client.waitForVerdict(jobId, waitSeconds * 1e3) : await client.getVerdict(jobId);
|
|
621
|
+
return text(v);
|
|
622
|
+
} catch (e) {
|
|
623
|
+
return fail(e);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
);
|
|
627
|
+
server.registerTool(
|
|
628
|
+
"vouch_approve",
|
|
629
|
+
{
|
|
630
|
+
title: "Approve and pay",
|
|
631
|
+
description: "As the payer, release payment to the worker now (signs a Settle authorisation with the agent wallet; Vouch relays it). Use after reading the verdict, or to pay early.",
|
|
632
|
+
inputSchema: { jobId: z4.string() }
|
|
633
|
+
},
|
|
634
|
+
async ({ jobId }) => {
|
|
635
|
+
try {
|
|
636
|
+
const r = await client.approve(jobId);
|
|
637
|
+
return text({ status: r.status, tx: r.tx, job: summarise(r.job) });
|
|
638
|
+
} catch (e) {
|
|
639
|
+
return fail(e);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
server.registerTool(
|
|
644
|
+
"vouch_dispute",
|
|
645
|
+
{
|
|
646
|
+
title: "Open a dispute",
|
|
647
|
+
description: "Either party objects to the delivery or the verdict. An arbiter reviews the same pinned evidence and splits the locked amount. Give a concrete reason (\u2265 10 characters).",
|
|
648
|
+
inputSchema: { jobId: z4.string(), reason: z4.string().min(10).max(5e3) }
|
|
649
|
+
},
|
|
650
|
+
async ({ jobId, reason }) => {
|
|
651
|
+
try {
|
|
652
|
+
const r = await client.dispute(jobId, reason);
|
|
653
|
+
return text({ status: r.status, tx: r.tx, job: summarise(r.job) });
|
|
654
|
+
} catch (e) {
|
|
655
|
+
return fail(e);
|
|
656
|
+
}
|
|
27
657
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
658
|
+
);
|
|
659
|
+
server.registerTool(
|
|
660
|
+
"vouch_get_job",
|
|
661
|
+
{ title: "Get a job", description: "Current status, pill, policy, timestamps, transactions and your role on the job.", inputSchema: { jobId: z4.string(), includeTimeline: z4.boolean().default(false) } },
|
|
662
|
+
async ({ jobId, includeTimeline }) => {
|
|
663
|
+
try {
|
|
664
|
+
const { job } = await client.getJob(jobId);
|
|
665
|
+
const timeline = includeTimeline ? (await client.getTimeline(jobId)).events : void 0;
|
|
666
|
+
return text({ ...summarise(job), scope: job.scopeMd, policy: job.policy, submitDeadlineAt: job.submitDeadlineAt, fundRoutes: job.fundRoutes, timeline });
|
|
667
|
+
} catch (e) {
|
|
668
|
+
return fail(e);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
server.registerTool(
|
|
673
|
+
"vouch_list_jobs",
|
|
674
|
+
{ title: "List my jobs", description: "Jobs where the agent wallet is payer or worker.", inputSchema: { status: z4.string().optional().describe("Filter: Open | Funded | Submitted | Attested | Settled | Disputed | Resolved | Refunded") } },
|
|
675
|
+
async ({ status }) => {
|
|
676
|
+
try {
|
|
677
|
+
const { jobs } = await client.listJobs(status);
|
|
678
|
+
return text({ count: jobs.length, jobs: jobs.map(summarise) });
|
|
679
|
+
} catch (e) {
|
|
680
|
+
return fail(e);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
server.registerTool(
|
|
685
|
+
"vouch_list_earn_vaults",
|
|
686
|
+
{ title: "List Earn vaults", description: "Tempo Earn vaults a job may use for 'Earn while locked': label, venue, APY, and whether the Vouch vault allow-lists it on-chain.", inputSchema: { chainId: z4.number().int().optional() } },
|
|
687
|
+
async ({ chainId }) => {
|
|
688
|
+
try {
|
|
689
|
+
const r = await client.request("GET", `/earn/vaults?chainId=${chainId ?? cfg.VOUCH_DEFAULT_CHAIN}`);
|
|
690
|
+
return text(r);
|
|
691
|
+
} catch (e) {
|
|
692
|
+
return fail(e);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
server.registerResource(
|
|
697
|
+
"job",
|
|
698
|
+
new ResourceTemplate("vouch://job/{id}", { list: void 0 }),
|
|
699
|
+
{ title: "Vouch job", description: "Job status, scope and policy as JSON", mimeType: "application/json" },
|
|
700
|
+
async (uri, { id }) => {
|
|
701
|
+
const { job } = await client.getJob(String(id));
|
|
702
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(job, null, 2) }] };
|
|
703
|
+
}
|
|
704
|
+
);
|
|
705
|
+
server.registerResource(
|
|
706
|
+
"verdict",
|
|
707
|
+
new ResourceTemplate("vouch://verdict/{id}", { list: void 0 }),
|
|
708
|
+
{ title: "Vouch verdict", description: "Verifier report for a job as JSON", mimeType: "application/json" },
|
|
709
|
+
async (uri, { id }) => {
|
|
710
|
+
const v = await client.getVerdict(String(id));
|
|
711
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(v, null, 2) }] };
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
server.registerPrompt(
|
|
715
|
+
"hire_for_task",
|
|
716
|
+
{
|
|
717
|
+
title: "Hire for a task",
|
|
718
|
+
description: "Guides an agent through create \u2192 fund \u2192 wait \u2192 verdict \u2192 approve/dispute for a piece of work.",
|
|
719
|
+
argsSchema: { task: z4.string().describe("What you need done"), budget: z4.string().describe("Dollar budget, e.g. 5"), worker: z4.string().optional().describe("Worker address/URL if known") }
|
|
720
|
+
},
|
|
721
|
+
({ task, budget, worker }) => ({
|
|
722
|
+
messages: [
|
|
723
|
+
{
|
|
724
|
+
role: "user",
|
|
725
|
+
content: {
|
|
726
|
+
type: "text",
|
|
727
|
+
text: [
|
|
728
|
+
`You are hiring for: ${task}. Budget: $${budget}.${worker ? ` Worker: ${worker}.` : ""}`,
|
|
729
|
+
"",
|
|
730
|
+
"Follow these steps with the vouch_* tools:",
|
|
731
|
+
"1. Write a precise scope in markdown with sections: Deliverables, Format, Deadline, Out of scope. Be concrete: the verifier checks each line.",
|
|
732
|
+
'2. vouch_create_job with that scope, the budget, and policyPreset "autopilot" (or "trusted" if budget > $50). Share the payUrl if a human worker needs it.',
|
|
733
|
+
"3. vouch_fund_job to lock the money. Tell the worker the jobId.",
|
|
734
|
+
"4. When the worker delivers, vouch_get_verdict (it waits for the verifier).",
|
|
735
|
+
"5. If PASS and the policy auto-settles, do nothing \u2014 the vault pays after the review window. If you are satisfied earlier, vouch_approve.",
|
|
736
|
+
"6. If NEEDS_REVIEW, read the checklist and questions_for_worker; either vouch_approve, ask the worker to resubmit, or vouch_dispute with a specific reason.",
|
|
737
|
+
"7. If FAIL, wait for a resubmission (max 2) or let the delivery deadline refund you.",
|
|
738
|
+
"Report every jobId, verdict and transaction hash you receive."
|
|
739
|
+
].join("\n")
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
]
|
|
743
|
+
})
|
|
744
|
+
);
|
|
745
|
+
return server;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/index.ts
|
|
749
|
+
async function main() {
|
|
750
|
+
const cfg = loadConfig();
|
|
751
|
+
if (cfg.VOUCH_HTTP_PORT) {
|
|
752
|
+
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
753
|
+
const http = createHttpServer(async (req, res) => {
|
|
754
|
+
if (!req.url?.startsWith("/mcp")) {
|
|
755
|
+
res.writeHead(404).end();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const server2 = createServer(cfg);
|
|
759
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
760
|
+
res.on("close", () => {
|
|
761
|
+
void transport.close();
|
|
762
|
+
void server2.close();
|
|
763
|
+
});
|
|
764
|
+
await server2.connect(transport);
|
|
765
|
+
await transport.handleRequest(req, res);
|
|
766
|
+
});
|
|
767
|
+
http.listen(cfg.VOUCH_HTTP_PORT, () => console.error(`vouch-mcp listening on http://localhost:${cfg.VOUCH_HTTP_PORT}/mcp`));
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const server = createServer(cfg);
|
|
771
|
+
await server.connect(new StdioServerTransport());
|
|
772
|
+
console.error(`vouch-mcp ready (stdio) \u2192 ${cfg.VOUCH_API_URL}`);
|
|
31
773
|
}
|
|
32
|
-
|
|
774
|
+
export {
|
|
775
|
+
VouchClient,
|
|
776
|
+
VouchError,
|
|
777
|
+
createServer,
|
|
778
|
+
loadConfig,
|
|
779
|
+
main
|
|
780
|
+
};
|