@klappay/mcp 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/dist/chunk-FCIY5UOM.js +9 -0
- package/dist/chunk-FCIY5UOM.js.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/start-4SOOSFNC.js +620 -0
- package/dist/start-4SOOSFNC.js.map +1 -0
- package/docs/configuration.md +79 -0
- package/docs/getting-started.md +80 -0
- package/docs/public/favicon.png +0 -0
- package/docs/public/logo.png +0 -0
- package/docs/security.md +66 -0
- package/docs/tools.md +131 -0
- package/package.json +69 -4
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
SERVER_NAME
|
|
4
|
+
} from "./chunk-FCIY5UOM.js";
|
|
5
|
+
|
|
6
|
+
// src/start.ts
|
|
7
|
+
import { loadCredentials } from "@klappay/cli/credentials";
|
|
8
|
+
import { createClient } from "@klappay/node";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
|
|
11
|
+
// src/environment.ts
|
|
12
|
+
import {
|
|
13
|
+
CREDENTIALS_DISPLAY_PATH,
|
|
14
|
+
KlapCredentialsError,
|
|
15
|
+
detectEnvironment
|
|
16
|
+
} from "@klappay/cli/credentials";
|
|
17
|
+
var LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
18
|
+
var LIVE_WRITES_ENABLED_VALUE = "1";
|
|
19
|
+
var LOGIN_COMMAND = "`klap login --api-key - --base-url <url>`";
|
|
20
|
+
var LOGIN_HINT = `Run ${LOGIN_COMMAND} (from @klappay/cli) or set KLAP_API_KEY and KLAP_BASE_URL.`;
|
|
21
|
+
var CREDENTIALS_ERROR_MESSAGES = {
|
|
22
|
+
invalid_api_key_prefix: `A key in ${CREDENTIALS_DISPLAY_PATH} does not start with klap_test_ or klap_live_. Run ${LOGIN_COMMAND} again.`,
|
|
23
|
+
invalid_credentials_file: `${CREDENTIALS_DISPLAY_PATH} is corrupted or has a key in the wrong environment slot. Run \`klap logout\` to remove it, then ${LOGIN_COMMAND} again.`,
|
|
24
|
+
credentials_path_symlink: `Refusing to read ${CREDENTIALS_DISPLAY_PATH}: it (or ~/.klap) is a symbolic link. Remove the link, then run ${LOGIN_COMMAND} to create a real ~/.klap directory.`,
|
|
25
|
+
no_credentials: `No API key in ${CREDENTIALS_DISPLAY_PATH}. ${LOGIN_HINT}`,
|
|
26
|
+
missing_environment_key: `${CREDENTIALS_DISPLAY_PATH} has no key for the requested environment. Run ${LOGIN_COMMAND} with that key, or change KLAP_ENV.`,
|
|
27
|
+
ambiguous_environment: `${CREDENTIALS_DISPLAY_PATH} has keys for both environments; set KLAP_ENV to test or live.`
|
|
28
|
+
};
|
|
29
|
+
function credentialsErrorMessage(err) {
|
|
30
|
+
if (Object.hasOwn(CREDENTIALS_ERROR_MESSAGES, err.code))
|
|
31
|
+
return CREDENTIALS_ERROR_MESSAGES[err.code];
|
|
32
|
+
return `Could not use ${CREDENTIALS_DISPLAY_PATH} (${err.code}). Run \`klap logout\`, then ${LOGIN_COMMAND} again.`;
|
|
33
|
+
}
|
|
34
|
+
function refuse(message) {
|
|
35
|
+
return { ok: false, message };
|
|
36
|
+
}
|
|
37
|
+
function presentValue(value) {
|
|
38
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
39
|
+
}
|
|
40
|
+
function parseEnvironment(value) {
|
|
41
|
+
if (value === void 0) return "test";
|
|
42
|
+
if (value === "test" || value === "live") return value;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function keyEnvironment(apiKey) {
|
|
46
|
+
try {
|
|
47
|
+
return detectEnvironment(apiKey);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function baseUrlProblem(value, label) {
|
|
53
|
+
let url;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(value);
|
|
56
|
+
} catch {
|
|
57
|
+
return `${label} is not a valid URL.`;
|
|
58
|
+
}
|
|
59
|
+
if (url.username !== "" || url.password !== "") {
|
|
60
|
+
return `${label} must not contain a username or password.`;
|
|
61
|
+
}
|
|
62
|
+
if (url.protocol === "https:") return null;
|
|
63
|
+
if (url.protocol === "http:" && LOOPBACK_HOSTNAMES.includes(url.hostname)) return null;
|
|
64
|
+
return `${label} must use https:// (http:// is only allowed for localhost, 127.0.0.1 or [::1]).`;
|
|
65
|
+
}
|
|
66
|
+
function normalizedBaseUrl(value) {
|
|
67
|
+
const url = new URL(value);
|
|
68
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
69
|
+
}
|
|
70
|
+
function keyMismatchMessage(source, keyEnv, wanted) {
|
|
71
|
+
return `${source} is a ${keyEnv} key, but the server is set to ${wanted} (KLAP_ENV${wanted === "test" ? " unset or test" : "=live"}). Refusing to start.`;
|
|
72
|
+
}
|
|
73
|
+
function resolveFromEnvKey(apiKey, baseUrl, environment, allowLiveWrites) {
|
|
74
|
+
if (baseUrl === void 0) {
|
|
75
|
+
return refuse("KLAP_API_KEY is set but KLAP_BASE_URL is not; set both.");
|
|
76
|
+
}
|
|
77
|
+
const urlProblem = baseUrlProblem(baseUrl, "KLAP_BASE_URL");
|
|
78
|
+
if (urlProblem) return refuse(urlProblem);
|
|
79
|
+
const keyEnv = keyEnvironment(apiKey);
|
|
80
|
+
if (keyEnv === null) return refuse("KLAP_API_KEY must start with klap_test_ or klap_live_.");
|
|
81
|
+
if (keyEnv !== environment) return refuse(keyMismatchMessage("KLAP_API_KEY", keyEnv, environment));
|
|
82
|
+
return { ok: true, apiKey, baseUrl, environment, allowLiveWrites, keySource: "env" };
|
|
83
|
+
}
|
|
84
|
+
async function loadConfigOrRefusal(loadConfig) {
|
|
85
|
+
try {
|
|
86
|
+
const config = await loadConfig();
|
|
87
|
+
return config ?? refuse(`No ${CREDENTIALS_DISPLAY_PATH} found. ${LOGIN_HINT}`);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof KlapCredentialsError) return refuse(credentialsErrorMessage(err));
|
|
90
|
+
const code = err instanceof Error && "code" in err ? String(err.code) : "unknown error";
|
|
91
|
+
return refuse(`Could not read ${CREDENTIALS_DISPLAY_PATH} (${code}).`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function resolveFromConfig(loadConfig, overrideBaseUrl, environment, allowLiveWrites) {
|
|
95
|
+
const loaded = await loadConfigOrRefusal(loadConfig);
|
|
96
|
+
if ("ok" in loaded) return loaded;
|
|
97
|
+
const apiKey = loaded.apiKeys[environment];
|
|
98
|
+
if (apiKey === void 0 || apiKey === "") {
|
|
99
|
+
return refuse(
|
|
100
|
+
`${CREDENTIALS_DISPLAY_PATH} has no ${environment} key. Run ${LOGIN_COMMAND} with a klap_${environment}_ key, or set KLAP_ENV to the environment you have a key for.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const keyEnv = keyEnvironment(apiKey);
|
|
104
|
+
if (keyEnv !== environment) {
|
|
105
|
+
return refuse(
|
|
106
|
+
`The ${environment} slot in ${CREDENTIALS_DISPLAY_PATH} does not hold a klap_${environment}_ key. Run ${LOGIN_COMMAND} again.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const urlProblem = baseUrlProblem(loaded.baseUrl, `The baseUrl in ${CREDENTIALS_DISPLAY_PATH}`);
|
|
110
|
+
if (urlProblem) return refuse(urlProblem);
|
|
111
|
+
if (overrideBaseUrl !== void 0) {
|
|
112
|
+
const overrideProblem = baseUrlProblem(overrideBaseUrl, "KLAP_BASE_URL");
|
|
113
|
+
if (overrideProblem) return refuse(overrideProblem);
|
|
114
|
+
if (normalizedBaseUrl(overrideBaseUrl) !== normalizedBaseUrl(loaded.baseUrl)) {
|
|
115
|
+
return refuse(
|
|
116
|
+
`KLAP_BASE_URL differs from the baseUrl stored in ${CREDENTIALS_DISPLAY_PATH}. A stored key is only ever sent to the base URL it was saved with \u2014 unset KLAP_BASE_URL, or set KLAP_API_KEY together with KLAP_BASE_URL instead.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
apiKey,
|
|
123
|
+
baseUrl: loaded.baseUrl,
|
|
124
|
+
environment,
|
|
125
|
+
allowLiveWrites,
|
|
126
|
+
keySource: "config"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function resolveServerEnvironment(vars, loadConfig) {
|
|
130
|
+
const environment = parseEnvironment(presentValue(vars.KLAP_ENV));
|
|
131
|
+
if (environment === null) return refuse('KLAP_ENV must be unset, "test" or "live".');
|
|
132
|
+
const allowLiveWrites = vars.KLAP_MCP_ALLOW_LIVE_WRITES === LIVE_WRITES_ENABLED_VALUE;
|
|
133
|
+
const apiKey = presentValue(vars.KLAP_API_KEY);
|
|
134
|
+
const baseUrl = presentValue(vars.KLAP_BASE_URL);
|
|
135
|
+
if (apiKey !== void 0) {
|
|
136
|
+
return resolveFromEnvKey(apiKey, baseUrl, environment, allowLiveWrites);
|
|
137
|
+
}
|
|
138
|
+
return resolveFromConfig(loadConfig, baseUrl, environment, allowLiveWrites);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/redact.ts
|
|
142
|
+
var REDACTED_URL = "[unparseable url]";
|
|
143
|
+
function redactUrl(value) {
|
|
144
|
+
let url;
|
|
145
|
+
try {
|
|
146
|
+
url = new URL(value);
|
|
147
|
+
} catch {
|
|
148
|
+
return REDACTED_URL;
|
|
149
|
+
}
|
|
150
|
+
if (url.origin === "null") return REDACTED_URL;
|
|
151
|
+
return `${url.origin}${url.pathname}`;
|
|
152
|
+
}
|
|
153
|
+
function hostOf(baseUrl) {
|
|
154
|
+
try {
|
|
155
|
+
return new URL(baseUrl).host;
|
|
156
|
+
} catch {
|
|
157
|
+
return REDACTED_URL;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/server.ts
|
|
162
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
163
|
+
|
|
164
|
+
// package.json
|
|
165
|
+
var version = "1.0.0";
|
|
166
|
+
|
|
167
|
+
// src/tool-policy.ts
|
|
168
|
+
function isToolAllowed(access, environment, allowLiveWrites) {
|
|
169
|
+
if (environment === "test") return true;
|
|
170
|
+
if (access === "read") return true;
|
|
171
|
+
if (access === "write") return allowLiveWrites;
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/tools/charges.ts
|
|
176
|
+
import {
|
|
177
|
+
ChargeSchema as ChargeSchema2,
|
|
178
|
+
CheckChargeRequestSchema,
|
|
179
|
+
CheckChargeResponseSchema,
|
|
180
|
+
CreateChargeSchema,
|
|
181
|
+
TimelineEventSchema
|
|
182
|
+
} from "@klappay/types";
|
|
183
|
+
import { z as z2 } from "zod";
|
|
184
|
+
|
|
185
|
+
// src/ids.ts
|
|
186
|
+
import { z } from "zod";
|
|
187
|
+
var ID_BODY_MAX_LENGTH = 64;
|
|
188
|
+
function prefixedIdSchema(prefix, label) {
|
|
189
|
+
return z.string().regex(
|
|
190
|
+
new RegExp(`^${prefix}_[A-Za-z0-9]{1,${ID_BODY_MAX_LENGTH}}$`),
|
|
191
|
+
`must be a ${label} id like ${prefix}_...`
|
|
192
|
+
).describe(`A Klappay ${label} id (\`${prefix}_\` followed by letters and digits).`);
|
|
193
|
+
}
|
|
194
|
+
var ChargeIdSchema = prefixedIdSchema("ch", "charge");
|
|
195
|
+
var WebhookIdSchema = prefixedIdSchema("wh", "webhook");
|
|
196
|
+
var WebhookDeliveryIdSchema = prefixedIdSchema("ev", "webhook delivery");
|
|
197
|
+
|
|
198
|
+
// src/result.ts
|
|
199
|
+
import { KlapApiError } from "@klappay/node";
|
|
200
|
+
import { ZodError } from "zod";
|
|
201
|
+
var UNEXPECTED_ERROR_MESSAGE = "Unexpected error calling the Klap API";
|
|
202
|
+
var UNEXPECTED_RESPONSE_CODE = "unexpected_response";
|
|
203
|
+
var VALIDATION_ERROR_CODE = "validation_error";
|
|
204
|
+
var ToolInputError = class extends Error {
|
|
205
|
+
constructor(message) {
|
|
206
|
+
super(message);
|
|
207
|
+
this.name = "ToolInputError";
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
function toResult(payload, isError) {
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
213
|
+
structuredContent: payload,
|
|
214
|
+
...isError ? { isError: true } : {}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function successResult(environment, data) {
|
|
218
|
+
return toResult({ ...data, environment }, false);
|
|
219
|
+
}
|
|
220
|
+
function describeForLog(err) {
|
|
221
|
+
if (!(err instanceof Error)) return typeof err;
|
|
222
|
+
const cause = err.cause instanceof Error ? ` (cause: ${err.cause.name}: ${err.cause.message})` : "";
|
|
223
|
+
return `${err.name}: ${err.message}${cause}`;
|
|
224
|
+
}
|
|
225
|
+
function zodIssuePaths(err) {
|
|
226
|
+
return err.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.code}`).join("; ");
|
|
227
|
+
}
|
|
228
|
+
function errorResult(environment, err) {
|
|
229
|
+
if (err instanceof KlapApiError) {
|
|
230
|
+
return toResult(
|
|
231
|
+
{ environment, error: { code: err.code, status: err.status, message: err.message } },
|
|
232
|
+
true
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (err instanceof ToolInputError) {
|
|
236
|
+
return toResult(
|
|
237
|
+
{ environment, error: { code: VALIDATION_ERROR_CODE, message: err.message } },
|
|
238
|
+
true
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (err instanceof ZodError) {
|
|
242
|
+
console.error(`klap-mcp: API response did not match the expected schema: ${zodIssuePaths(err)}`);
|
|
243
|
+
return toResult(
|
|
244
|
+
{
|
|
245
|
+
environment,
|
|
246
|
+
error: {
|
|
247
|
+
code: UNEXPECTED_RESPONSE_CODE,
|
|
248
|
+
message: "The Klap API returned a response this server does not recognize."
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
true
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
console.error(`klap-mcp: ${UNEXPECTED_ERROR_MESSAGE}: ${describeForLog(err)}`);
|
|
255
|
+
return toResult(
|
|
256
|
+
{ environment, error: { code: "unexpected_error", message: UNEXPECTED_ERROR_MESSAGE } },
|
|
257
|
+
true
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
async function runTool(environment, action) {
|
|
261
|
+
try {
|
|
262
|
+
return successResult(environment, await action());
|
|
263
|
+
} catch (err) {
|
|
264
|
+
return errorResult(environment, err);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/tools/charge-output.ts
|
|
269
|
+
import { ChargeSchema } from "@klappay/types";
|
|
270
|
+
var ChargeWithoutMetadataSchema = ChargeSchema.omit({ metadata: true });
|
|
271
|
+
|
|
272
|
+
// src/tools/charges.ts
|
|
273
|
+
var CheckResponseWithoutMetadataSchema = CheckChargeResponseSchema.omit({ metadata: true });
|
|
274
|
+
var TimelineSchema = z2.array(TimelineEventSchema);
|
|
275
|
+
var CreateChargeToolInputSchema = CreateChargeSchema.innerType().omit({ escrow: true, redirectUrl: true }).strict();
|
|
276
|
+
var GetChargeInputSchema = z2.object({
|
|
277
|
+
id: ChargeIdSchema,
|
|
278
|
+
includeMetadata: z2.boolean().default(false).describe(
|
|
279
|
+
"Include the charge's free-form `metadata` object (merchant-supplied data, may hold customer details). Omitted unless true."
|
|
280
|
+
)
|
|
281
|
+
}).strict();
|
|
282
|
+
var ChargeIdInputSchema = z2.object({ id: ChargeIdSchema }).strict();
|
|
283
|
+
var CheckChargeInputSchema = CheckChargeRequestSchema.innerType().extend({ id: ChargeIdSchema }).strict();
|
|
284
|
+
function validationMessage(error) {
|
|
285
|
+
return error.issues.map((issue) => issue.message).join("; ");
|
|
286
|
+
}
|
|
287
|
+
var chargeTools = [
|
|
288
|
+
{
|
|
289
|
+
name: "charges_get",
|
|
290
|
+
access: "read",
|
|
291
|
+
register(server, { client, environment }) {
|
|
292
|
+
server.registerTool(
|
|
293
|
+
"charges_get",
|
|
294
|
+
{
|
|
295
|
+
title: "Get charge",
|
|
296
|
+
description: "Fetch one charge by id: status, settlement status, amounts, accepted payment pairs, split recipients and timestamps. `metadata` is left out unless includeMetadata is true.",
|
|
297
|
+
inputSchema: GetChargeInputSchema,
|
|
298
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
299
|
+
},
|
|
300
|
+
({ id, includeMetadata }) => runTool(environment, async () => {
|
|
301
|
+
const charge = await client.charges.get(id);
|
|
302
|
+
const schema = includeMetadata ? ChargeSchema2 : ChargeWithoutMetadataSchema;
|
|
303
|
+
return { charge: schema.parse(charge) };
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "charges_timeline",
|
|
310
|
+
access: "read",
|
|
311
|
+
register(server, { client, environment }) {
|
|
312
|
+
server.registerTool(
|
|
313
|
+
"charges_timeline",
|
|
314
|
+
{
|
|
315
|
+
title: "Get charge timeline",
|
|
316
|
+
description: "List the lifecycle events of one charge in order (created, transfers detected, confirmed, settled, ...).",
|
|
317
|
+
inputSchema: ChargeIdInputSchema,
|
|
318
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
319
|
+
},
|
|
320
|
+
({ id }) => runTool(environment, async () => ({
|
|
321
|
+
chargeId: id,
|
|
322
|
+
events: TimelineSchema.parse(await client.charges.getTimeline(id))
|
|
323
|
+
}))
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "charges_create",
|
|
329
|
+
access: "write",
|
|
330
|
+
register(server, { client, environment }) {
|
|
331
|
+
server.registerTool(
|
|
332
|
+
"charges_create",
|
|
333
|
+
{
|
|
334
|
+
title: "Create charge",
|
|
335
|
+
description: `Create a new ${environment} charge. Escrow charges and redirectUrl are not available through this server. Pass idempotencyKey to make a retry safe; without it every call creates a new charge.`,
|
|
336
|
+
inputSchema: CreateChargeToolInputSchema,
|
|
337
|
+
annotations: {
|
|
338
|
+
readOnlyHint: false,
|
|
339
|
+
destructiveHint: false,
|
|
340
|
+
idempotentHint: false,
|
|
341
|
+
openWorldHint: true
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
(input) => runTool(environment, async () => ({
|
|
345
|
+
charge: ChargeWithoutMetadataSchema.parse(await client.charges.create(input))
|
|
346
|
+
}))
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "charges_check",
|
|
352
|
+
access: "write",
|
|
353
|
+
register(server, { client, environment }) {
|
|
354
|
+
server.registerTool(
|
|
355
|
+
"charges_check",
|
|
356
|
+
{
|
|
357
|
+
title: "Re-check charge on-chain",
|
|
358
|
+
description: "Ask the API to re-check a charge's payment on-chain now instead of waiting for the background pass. Optionally pass txHash together with network to verify one specific transaction. Never credits anything that is not really on-chain.",
|
|
359
|
+
inputSchema: CheckChargeInputSchema,
|
|
360
|
+
annotations: {
|
|
361
|
+
readOnlyHint: false,
|
|
362
|
+
destructiveHint: false,
|
|
363
|
+
idempotentHint: true,
|
|
364
|
+
openWorldHint: true
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
({ id, ...hint }) => runTool(environment, async () => {
|
|
368
|
+
const request = CheckChargeRequestSchema.safeParse(hint);
|
|
369
|
+
if (!request.success) throw new ToolInputError(validationMessage(request.error));
|
|
370
|
+
const checked = await client.charges.check(id, request.data);
|
|
371
|
+
return { charge: CheckResponseWithoutMetadataSchema.parse(checked) };
|
|
372
|
+
})
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
// src/tools/metrics.ts
|
|
379
|
+
import { MetricsQueryResultSchema, MetricsQuerySchema } from "@klappay/types";
|
|
380
|
+
import { z as z3 } from "zod";
|
|
381
|
+
var MetricsQueryInputSchema = z3.object({
|
|
382
|
+
query: MetricsQuerySchema.describe(
|
|
383
|
+
"The metrics query: a resource (charges, transactions or distributions), metrics to aggregate, an ISO 8601 dateRange, optional filters and groupBy."
|
|
384
|
+
)
|
|
385
|
+
}).strict();
|
|
386
|
+
var metricsTools = [
|
|
387
|
+
{
|
|
388
|
+
name: "metrics_query",
|
|
389
|
+
access: "read",
|
|
390
|
+
register(server, { client, environment }) {
|
|
391
|
+
server.registerTool(
|
|
392
|
+
"metrics_query",
|
|
393
|
+
{
|
|
394
|
+
title: "Query business metrics",
|
|
395
|
+
description: "Run an aggregate query over this environment's charges, transactions or distributions (counts, sums, grouped by date bucket or dimension).",
|
|
396
|
+
inputSchema: MetricsQueryInputSchema,
|
|
397
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
398
|
+
},
|
|
399
|
+
({ query }) => runTool(
|
|
400
|
+
environment,
|
|
401
|
+
async () => MetricsQueryResultSchema.parse(await client.metrics.query(query))
|
|
402
|
+
)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
// src/tools/networks.ts
|
|
409
|
+
import { CapabilitiesSchema } from "@klappay/types";
|
|
410
|
+
var networkTools = [
|
|
411
|
+
{
|
|
412
|
+
name: "networks_get",
|
|
413
|
+
access: "read",
|
|
414
|
+
register(server, { client, environment }) {
|
|
415
|
+
server.registerTool(
|
|
416
|
+
"networks_get",
|
|
417
|
+
{
|
|
418
|
+
title: "Get accepted networks",
|
|
419
|
+
description: "List the (token, network) payment pairs this environment accepts right now \u2014 the same list charges_create validates acceptedPayments against.",
|
|
420
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
421
|
+
},
|
|
422
|
+
() => runTool(environment, async () => CapabilitiesSchema.parse(await client.networks.get()))
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
// src/tools/sandbox.ts
|
|
429
|
+
import { SandboxTriggerSchema } from "@klappay/types";
|
|
430
|
+
var SandboxTriggerInputSchema = SandboxTriggerSchema.extend({ chargeId: ChargeIdSchema }).strict();
|
|
431
|
+
var sandboxTools = [
|
|
432
|
+
{
|
|
433
|
+
name: "sandbox_trigger",
|
|
434
|
+
access: "sandbox",
|
|
435
|
+
register(server, { client, environment }) {
|
|
436
|
+
server.registerTool(
|
|
437
|
+
"sandbox_trigger",
|
|
438
|
+
{
|
|
439
|
+
title: "Simulate sandbox charge event",
|
|
440
|
+
description: "Push a test charge into a payment state (confirmed, partially paid, overpaid, expired, underpaid, settled, settlement failed) without moving funds, and fire the matching webhook. Test environment only.",
|
|
441
|
+
inputSchema: SandboxTriggerInputSchema,
|
|
442
|
+
annotations: {
|
|
443
|
+
readOnlyHint: false,
|
|
444
|
+
destructiveHint: true,
|
|
445
|
+
idempotentHint: false,
|
|
446
|
+
openWorldHint: true
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
({ chargeId, event, amount }) => runTool(environment, async () => ({
|
|
450
|
+
charge: ChargeWithoutMetadataSchema.parse(
|
|
451
|
+
await client.sandbox.trigger(chargeId, event, amount)
|
|
452
|
+
)
|
|
453
|
+
}))
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
// src/tools/status.ts
|
|
460
|
+
var statusTools = [
|
|
461
|
+
{
|
|
462
|
+
name: "klap_status",
|
|
463
|
+
access: "read",
|
|
464
|
+
register(server, { environment, baseUrl }) {
|
|
465
|
+
server.registerTool(
|
|
466
|
+
"klap_status",
|
|
467
|
+
{
|
|
468
|
+
title: "Klap connection status",
|
|
469
|
+
description: "Show which Klap environment (test or live) and API host this server is connected to. Makes no API call.",
|
|
470
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
471
|
+
},
|
|
472
|
+
() => successResult(environment, { host: hostOf(baseUrl) })
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
];
|
|
477
|
+
|
|
478
|
+
// src/tools/webhooks.ts
|
|
479
|
+
import {
|
|
480
|
+
ListWebhookDeliveriesSchema,
|
|
481
|
+
PaginatedWebhookDeliveriesSchema,
|
|
482
|
+
WebhookListItemSchema
|
|
483
|
+
} from "@klappay/types";
|
|
484
|
+
import { z as z4 } from "zod";
|
|
485
|
+
var WebhookListSchema = z4.array(WebhookListItemSchema);
|
|
486
|
+
var ListDeliveriesInputSchema = ListWebhookDeliveriesSchema.extend({
|
|
487
|
+
webhookId: WebhookIdSchema
|
|
488
|
+
}).strict();
|
|
489
|
+
var RetryDeliveryInputSchema = z4.object({ webhookId: WebhookIdSchema, deliveryId: WebhookDeliveryIdSchema }).strict();
|
|
490
|
+
var webhookTools = [
|
|
491
|
+
{
|
|
492
|
+
name: "webhooks_list",
|
|
493
|
+
access: "read",
|
|
494
|
+
register(server, { client, environment }) {
|
|
495
|
+
server.registerTool(
|
|
496
|
+
"webhooks_list",
|
|
497
|
+
{
|
|
498
|
+
title: "List webhooks",
|
|
499
|
+
description: "List webhook endpoints and the events they subscribe to. Endpoint URLs are shown as origin + path only (credentials and query strings removed); secrets are never returned, only their display hint.",
|
|
500
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
501
|
+
},
|
|
502
|
+
() => runTool(environment, async () => {
|
|
503
|
+
const webhooks = WebhookListSchema.parse(await client.webhooks.list());
|
|
504
|
+
return {
|
|
505
|
+
webhooks: webhooks.map((webhook) => ({ ...webhook, url: redactUrl(webhook.url) }))
|
|
506
|
+
};
|
|
507
|
+
})
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "webhooks_list_deliveries",
|
|
513
|
+
access: "read",
|
|
514
|
+
register(server, { client, environment }) {
|
|
515
|
+
server.registerTool(
|
|
516
|
+
"webhooks_list_deliveries",
|
|
517
|
+
{
|
|
518
|
+
title: "List webhook deliveries",
|
|
519
|
+
description: "List one webhook's delivery attempts (event, status, attempts, response code), newest first. Paginate with limit and the previous page's nextCursor.",
|
|
520
|
+
inputSchema: ListDeliveriesInputSchema,
|
|
521
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
522
|
+
},
|
|
523
|
+
({ webhookId, ...page }) => runTool(environment, async () => {
|
|
524
|
+
const deliveries = await client.webhooks.listDeliveries(webhookId, page);
|
|
525
|
+
return { webhookId, ...PaginatedWebhookDeliveriesSchema.parse(deliveries) };
|
|
526
|
+
})
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "webhooks_retry_delivery",
|
|
532
|
+
access: "write",
|
|
533
|
+
register(server, { client, environment }) {
|
|
534
|
+
server.registerTool(
|
|
535
|
+
"webhooks_retry_delivery",
|
|
536
|
+
{
|
|
537
|
+
title: "Retry webhook delivery",
|
|
538
|
+
description: "Re-send one past webhook delivery to its endpoint now. The receiving system will process the event again.",
|
|
539
|
+
inputSchema: RetryDeliveryInputSchema,
|
|
540
|
+
annotations: {
|
|
541
|
+
readOnlyHint: false,
|
|
542
|
+
destructiveHint: true,
|
|
543
|
+
idempotentHint: false,
|
|
544
|
+
openWorldHint: true
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
({ webhookId, deliveryId }) => runTool(environment, async () => {
|
|
548
|
+
await client.webhooks.retryDelivery(webhookId, deliveryId);
|
|
549
|
+
return { webhookId, deliveryId, retried: true };
|
|
550
|
+
})
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
];
|
|
555
|
+
|
|
556
|
+
// src/server.ts
|
|
557
|
+
var ALL_TOOLS = [
|
|
558
|
+
...statusTools,
|
|
559
|
+
...chargeTools,
|
|
560
|
+
...webhookTools,
|
|
561
|
+
...networkTools,
|
|
562
|
+
...metricsTools,
|
|
563
|
+
...sandboxTools
|
|
564
|
+
];
|
|
565
|
+
function modeDescription(environment, allowLiveWrites) {
|
|
566
|
+
if (environment === "test") {
|
|
567
|
+
return "This is the TEST environment: sandbox data and no real funds. Read tools, charges_create, charges_check, webhooks_retry_delivery and sandbox_trigger are available.";
|
|
568
|
+
}
|
|
569
|
+
if (allowLiveWrites) {
|
|
570
|
+
return "This is the LIVE environment: real merchants, real payers and real funds. Live writes were explicitly enabled (KLAP_MCP_ALLOW_LIVE_WRITES=1): charges_create, charges_check and webhooks_retry_delivery act on real data \u2014 confirm with the user before calling them.";
|
|
571
|
+
}
|
|
572
|
+
return "This is the LIVE environment: real merchants, real payers and real funds. The server is read-only in live; write tools are not registered unless KLAP_MCP_ALLOW_LIVE_WRITES=1 is set.";
|
|
573
|
+
}
|
|
574
|
+
function buildInstructions(environment, baseUrl, allowLiveWrites) {
|
|
575
|
+
return [
|
|
576
|
+
`Klap Core API (${hostOf(baseUrl)}), environment: ${environment}.`,
|
|
577
|
+
modeDescription(environment, allowLiveWrites),
|
|
578
|
+
"Moving money out of a charge (escrow release/refund) and managing webhook endpoints or payout recipients are not available through this server."
|
|
579
|
+
].join(" ");
|
|
580
|
+
}
|
|
581
|
+
function createServer(options) {
|
|
582
|
+
const { client, environment, baseUrl, allowLiveWrites } = options;
|
|
583
|
+
const server = new McpServer(
|
|
584
|
+
{ name: SERVER_NAME, version },
|
|
585
|
+
{ instructions: buildInstructions(environment, baseUrl, allowLiveWrites) }
|
|
586
|
+
);
|
|
587
|
+
for (const tool of ALL_TOOLS) {
|
|
588
|
+
if (isToolAllowed(tool.access, environment, allowLiveWrites)) {
|
|
589
|
+
tool.register(server, { client, environment, baseUrl });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return server;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/start.ts
|
|
596
|
+
async function start() {
|
|
597
|
+
const resolved = await resolveServerEnvironment(
|
|
598
|
+
{
|
|
599
|
+
KLAP_ENV: process.env.KLAP_ENV,
|
|
600
|
+
KLAP_API_KEY: process.env.KLAP_API_KEY,
|
|
601
|
+
KLAP_BASE_URL: process.env.KLAP_BASE_URL,
|
|
602
|
+
KLAP_MCP_ALLOW_LIVE_WRITES: process.env.KLAP_MCP_ALLOW_LIVE_WRITES
|
|
603
|
+
},
|
|
604
|
+
loadCredentials
|
|
605
|
+
);
|
|
606
|
+
if (!resolved.ok) {
|
|
607
|
+
console.error(`${SERVER_NAME}: ${resolved.message}`);
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
const { apiKey, baseUrl, environment, allowLiveWrites } = resolved;
|
|
611
|
+
const client = createClient({ apiKey, baseUrl });
|
|
612
|
+
const server = createServer({ client, environment, baseUrl, allowLiveWrites });
|
|
613
|
+
await server.connect(new StdioServerTransport());
|
|
614
|
+
const writes = environment === "live" && allowLiveWrites ? " (live writes enabled)" : "";
|
|
615
|
+
console.error(`${SERVER_NAME}: ${environment} \u2192 ${hostOf(baseUrl)}${writes}`);
|
|
616
|
+
}
|
|
617
|
+
export {
|
|
618
|
+
start
|
|
619
|
+
};
|
|
620
|
+
//# sourceMappingURL=start-4SOOSFNC.js.map
|