splitwise-mcp 3.5.0 → 3.5.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +174 -75
- package/dist/index.js +1 -1
- package/dist/tools/_receipt.js +16 -4
- package/dist/tools/uploads.js +5 -6
- package/package.json +2 -2
- package/server.json +2 -2
- package/dist/tools/_blob.js +0 -40
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Splitwise — natural-language expense and group management via the Splitwise API",
|
|
10
|
-
"version": "3.5.
|
|
10
|
+
"version": "3.5.1"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Splitwise",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for Splitwise — manage expenses, groups, and friends via natural language",
|
|
18
|
-
"version": "3.5.
|
|
18
|
+
"version": "3.5.1",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -2604,21 +2604,21 @@ var require_schemas = __commonJS({
|
|
|
2604
2604
|
function stripTabAndNewline2(value) {
|
|
2605
2605
|
return value.replace(asciiTabOrNewline2, "");
|
|
2606
2606
|
}
|
|
2607
|
-
function urlHostnameOk2(
|
|
2607
|
+
function urlHostnameOk2(url4, hostname3) {
|
|
2608
2608
|
hostname3.lastIndex = 0;
|
|
2609
|
-
return hostname3.test(
|
|
2609
|
+
return hostname3.test(url4.hostname);
|
|
2610
2610
|
}
|
|
2611
|
-
function urlProtocolOk2(
|
|
2611
|
+
function urlProtocolOk2(url4, protocol) {
|
|
2612
2612
|
protocol.lastIndex = 0;
|
|
2613
|
-
return protocol.test(
|
|
2613
|
+
return protocol.test(url4.protocol.endsWith(":") ? url4.protocol.slice(0, -1) : url4.protocol);
|
|
2614
2614
|
}
|
|
2615
2615
|
exports.$ZodURL = core.$constructor("$ZodURL", (inst, def) => {
|
|
2616
2616
|
exports.$ZodStringFormat.init(inst, def);
|
|
2617
2617
|
inst._zod.check = (payload) => {
|
|
2618
2618
|
try {
|
|
2619
2619
|
const trimmed = payload.value.trim();
|
|
2620
|
-
const
|
|
2621
|
-
if (
|
|
2620
|
+
const url4 = validateURL2(trimmed, def);
|
|
2621
|
+
if (url4 === exports.URL_BAD_FORMAT) {
|
|
2622
2622
|
payload.issues.push({
|
|
2623
2623
|
code: "invalid_format",
|
|
2624
2624
|
format: "url",
|
|
@@ -2629,7 +2629,7 @@ var require_schemas = __commonJS({
|
|
|
2629
2629
|
});
|
|
2630
2630
|
return;
|
|
2631
2631
|
}
|
|
2632
|
-
if (
|
|
2632
|
+
if (url4 === exports.URL_UNPARSEABLE) {
|
|
2633
2633
|
payload.issues.push({
|
|
2634
2634
|
code: "invalid_format",
|
|
2635
2635
|
format: "url",
|
|
@@ -2639,11 +2639,11 @@ var require_schemas = __commonJS({
|
|
|
2639
2639
|
});
|
|
2640
2640
|
return;
|
|
2641
2641
|
}
|
|
2642
|
-
if (
|
|
2642
|
+
if (url4 === true) {
|
|
2643
2643
|
payload.value = stripTabAndNewline2(trimmed);
|
|
2644
2644
|
return;
|
|
2645
2645
|
}
|
|
2646
|
-
if (def.hostname && !urlHostnameOk2(
|
|
2646
|
+
if (def.hostname && !urlHostnameOk2(url4, def.hostname)) {
|
|
2647
2647
|
payload.issues.push({
|
|
2648
2648
|
code: "invalid_format",
|
|
2649
2649
|
format: "url",
|
|
@@ -2654,7 +2654,7 @@ var require_schemas = __commonJS({
|
|
|
2654
2654
|
continue: !def.abort
|
|
2655
2655
|
});
|
|
2656
2656
|
}
|
|
2657
|
-
if (def.protocol && !urlProtocolOk2(
|
|
2657
|
+
if (def.protocol && !urlProtocolOk2(url4, def.protocol)) {
|
|
2658
2658
|
payload.issues.push({
|
|
2659
2659
|
code: "invalid_format",
|
|
2660
2660
|
format: "url",
|
|
@@ -2665,7 +2665,7 @@ var require_schemas = __commonJS({
|
|
|
2665
2665
|
continue: !def.abort
|
|
2666
2666
|
});
|
|
2667
2667
|
}
|
|
2668
|
-
payload.value = def.normalize ?
|
|
2668
|
+
payload.value = def.normalize ? url4.href : stripTabAndNewline2(trimmed);
|
|
2669
2669
|
return;
|
|
2670
2670
|
} catch (_2) {
|
|
2671
2671
|
payload.issues.push({
|
|
@@ -20109,7 +20109,7 @@ var require_schemas2 = __commonJS({
|
|
|
20109
20109
|
exports.uuidv4 = uuidv42;
|
|
20110
20110
|
exports.uuidv6 = uuidv62;
|
|
20111
20111
|
exports.uuidv7 = uuidv72;
|
|
20112
|
-
exports.url =
|
|
20112
|
+
exports.url = url4;
|
|
20113
20113
|
exports.httpUrl = httpUrl2;
|
|
20114
20114
|
exports.emoji = emoji3;
|
|
20115
20115
|
exports.nanoid = nanoid3;
|
|
@@ -20590,7 +20590,7 @@ var require_schemas2 = __commonJS({
|
|
|
20590
20590
|
core.$ZodURL.init(inst, def);
|
|
20591
20591
|
exports.ZodStringFormat.init(inst, def);
|
|
20592
20592
|
});
|
|
20593
|
-
function
|
|
20593
|
+
function url4(params) {
|
|
20594
20594
|
return core._url(exports.ZodURL, params);
|
|
20595
20595
|
}
|
|
20596
20596
|
function httpUrl2(params) {
|
|
@@ -64155,8 +64155,8 @@ var SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
|
64155
64155
|
});
|
|
64156
64156
|
return z.NEVER;
|
|
64157
64157
|
}
|
|
64158
|
-
}).refine((
|
|
64159
|
-
const u2 = new URL(
|
|
64158
|
+
}).refine((url4) => {
|
|
64159
|
+
const u2 = new URL(url4);
|
|
64160
64160
|
return u2.protocol !== "javascript:" && u2.protocol !== "data:" && u2.protocol !== "vbscript:";
|
|
64161
64161
|
}, { message: "URL cannot use javascript:, data:, or vbscript: scheme" });
|
|
64162
64162
|
var OAuthProtectedResourceMetadataSchema = z.looseObject({
|
|
@@ -81129,21 +81129,21 @@ var asciiTabOrNewline = /[\t\n\r]/g;
|
|
|
81129
81129
|
function stripTabAndNewline(value) {
|
|
81130
81130
|
return value.replace(asciiTabOrNewline, "");
|
|
81131
81131
|
}
|
|
81132
|
-
function urlHostnameOk(
|
|
81132
|
+
function urlHostnameOk(url4, hostname3) {
|
|
81133
81133
|
hostname3.lastIndex = 0;
|
|
81134
|
-
return hostname3.test(
|
|
81134
|
+
return hostname3.test(url4.hostname);
|
|
81135
81135
|
}
|
|
81136
|
-
function urlProtocolOk(
|
|
81136
|
+
function urlProtocolOk(url4, protocol) {
|
|
81137
81137
|
protocol.lastIndex = 0;
|
|
81138
|
-
return protocol.test(
|
|
81138
|
+
return protocol.test(url4.protocol.endsWith(":") ? url4.protocol.slice(0, -1) : url4.protocol);
|
|
81139
81139
|
}
|
|
81140
81140
|
var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
81141
81141
|
$ZodStringFormat.init(inst, def);
|
|
81142
81142
|
inst._zod.check = (payload) => {
|
|
81143
81143
|
try {
|
|
81144
81144
|
const trimmed = payload.value.trim();
|
|
81145
|
-
const
|
|
81146
|
-
if (
|
|
81145
|
+
const url4 = validateURL(trimmed, def);
|
|
81146
|
+
if (url4 === URL_BAD_FORMAT) {
|
|
81147
81147
|
payload.issues.push({
|
|
81148
81148
|
code: "invalid_format",
|
|
81149
81149
|
format: "url",
|
|
@@ -81154,7 +81154,7 @@ var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
81154
81154
|
});
|
|
81155
81155
|
return;
|
|
81156
81156
|
}
|
|
81157
|
-
if (
|
|
81157
|
+
if (url4 === URL_UNPARSEABLE) {
|
|
81158
81158
|
payload.issues.push({
|
|
81159
81159
|
code: "invalid_format",
|
|
81160
81160
|
format: "url",
|
|
@@ -81164,11 +81164,11 @@ var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
81164
81164
|
});
|
|
81165
81165
|
return;
|
|
81166
81166
|
}
|
|
81167
|
-
if (
|
|
81167
|
+
if (url4 === true) {
|
|
81168
81168
|
payload.value = stripTabAndNewline(trimmed);
|
|
81169
81169
|
return;
|
|
81170
81170
|
}
|
|
81171
|
-
if (def.hostname && !urlHostnameOk(
|
|
81171
|
+
if (def.hostname && !urlHostnameOk(url4, def.hostname)) {
|
|
81172
81172
|
payload.issues.push({
|
|
81173
81173
|
code: "invalid_format",
|
|
81174
81174
|
format: "url",
|
|
@@ -81179,7 +81179,7 @@ var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
81179
81179
|
continue: !def.abort
|
|
81180
81180
|
});
|
|
81181
81181
|
}
|
|
81182
|
-
if (def.protocol && !urlProtocolOk(
|
|
81182
|
+
if (def.protocol && !urlProtocolOk(url4, def.protocol)) {
|
|
81183
81183
|
payload.issues.push({
|
|
81184
81184
|
code: "invalid_format",
|
|
81185
81185
|
format: "url",
|
|
@@ -81190,7 +81190,7 @@ var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
81190
81190
|
continue: !def.abort
|
|
81191
81191
|
});
|
|
81192
81192
|
}
|
|
81193
|
-
payload.value = def.normalize ?
|
|
81193
|
+
payload.value = def.normalize ? url4.href : stripTabAndNewline(trimmed);
|
|
81194
81194
|
return;
|
|
81195
81195
|
} catch (_2) {
|
|
81196
81196
|
payload.issues.push({
|
|
@@ -100161,19 +100161,19 @@ function createApiClient(opts) {
|
|
|
100161
100161
|
`Refusing request to ${service}: path ${JSON.stringify(path.split(/[?#]/, 1)[0])} contains a ${hazard}, which URL normalisation would rewrite. Build paths with apiPath\`...\` so each value is one encoded segment.`
|
|
100162
100162
|
);
|
|
100163
100163
|
}
|
|
100164
|
-
const
|
|
100164
|
+
const url4 = `${base}${joinQuery(path, query)}`;
|
|
100165
100165
|
if (baseOrigin === void 0)
|
|
100166
|
-
return
|
|
100166
|
+
return url4;
|
|
100167
100167
|
let parsed;
|
|
100168
100168
|
try {
|
|
100169
|
-
parsed = new URL(
|
|
100169
|
+
parsed = new URL(url4);
|
|
100170
100170
|
} catch {
|
|
100171
100171
|
throw new Error(`Refusing request to ${service}: path ${JSON.stringify(path)} does not form a valid URL.`);
|
|
100172
100172
|
}
|
|
100173
100173
|
if (parsed.origin !== baseOrigin || parsed.username !== baseParts?.username || parsed.password !== baseParts?.password) {
|
|
100174
100174
|
throw new Error(`Refusing request to ${service}: path ${JSON.stringify(path)} resolves outside the base origin ${baseOrigin}. Paths must be relative to baseUrl (start them with "/").`);
|
|
100175
100175
|
}
|
|
100176
|
-
return
|
|
100176
|
+
return url4;
|
|
100177
100177
|
}
|
|
100178
100178
|
function resolveLocation(location, from) {
|
|
100179
100179
|
try {
|
|
@@ -100197,7 +100197,7 @@ function createApiClient(opts) {
|
|
|
100197
100197
|
async function send(method, path, opt, dispatch) {
|
|
100198
100198
|
const { body: reqBody, contentType } = encodeBody(opt);
|
|
100199
100199
|
const query = opt.query ? buildQueryString(opt.query) : "";
|
|
100200
|
-
const
|
|
100200
|
+
const url4 = resolveUrl(path, query);
|
|
100201
100201
|
const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
|
|
100202
100202
|
const fetchHop = (hopUrl, hopMethod, withBody, token, signal, credentialed = true) => {
|
|
100203
100203
|
if (!signal?.aborted)
|
|
@@ -100222,8 +100222,8 @@ function createApiClient(opts) {
|
|
|
100222
100222
|
};
|
|
100223
100223
|
const fetchWith = async (token, signal) => {
|
|
100224
100224
|
if (!manualRedirects)
|
|
100225
|
-
return fetchHop(
|
|
100226
|
-
let hopUrl =
|
|
100225
|
+
return fetchHop(url4, method, true, token, signal);
|
|
100226
|
+
let hopUrl = url4;
|
|
100227
100227
|
let hopMethod = method;
|
|
100228
100228
|
let withBody = true;
|
|
100229
100229
|
let credentialed = true;
|
|
@@ -100502,10 +100502,10 @@ function isSplitwiseHost(hostname3) {
|
|
|
100502
100502
|
const host = hostname3.toLowerCase();
|
|
100503
100503
|
return host === SPLITWISE_DOMAIN || host.endsWith(`.${SPLITWISE_DOMAIN}`);
|
|
100504
100504
|
}
|
|
100505
|
-
function parseAssetUrl(
|
|
100505
|
+
function parseAssetUrl(url4) {
|
|
100506
100506
|
let parsed;
|
|
100507
100507
|
try {
|
|
100508
|
-
parsed = new URL(
|
|
100508
|
+
parsed = new URL(url4);
|
|
100509
100509
|
} catch {
|
|
100510
100510
|
throw new Error("Splitwise returned an asset URL that could not be parsed");
|
|
100511
100511
|
}
|
|
@@ -100640,8 +100640,8 @@ var SplitwiseClient = class {
|
|
|
100640
100640
|
* `maxBytes` overrides the client-wide body cap for this download; over it,
|
|
100641
100641
|
* the read is refused (`ResponseTooLargeError`, `kind: 'too_large'`).
|
|
100642
100642
|
*/
|
|
100643
|
-
async fetchAsset(
|
|
100644
|
-
const target = parseAssetUrl(
|
|
100643
|
+
async fetchAsset(url4, opts = {}) {
|
|
100644
|
+
const target = parseAssetUrl(url4);
|
|
100645
100645
|
const authenticated = isSplitwiseHost(target.hostname);
|
|
100646
100646
|
const cacheKey = `${target.origin}|${authenticated ? "auth" : "anon"}`;
|
|
100647
100647
|
let api = this.assetClients.get(cacheKey);
|
|
@@ -101376,40 +101376,134 @@ function swUntrustedEnvelope(payload) {
|
|
|
101376
101376
|
// src/tools/_receipt.ts
|
|
101377
101377
|
import { createHash as createHash4 } from "crypto";
|
|
101378
101378
|
|
|
101379
|
-
//
|
|
101380
|
-
import { createHmac as createHmac3 } from "crypto";
|
|
101381
|
-
|
|
101382
|
-
|
|
101383
|
-
|
|
101384
|
-
|
|
101379
|
+
// node_modules/@chrischall/mcp-utils/dist/export/blob.js
|
|
101380
|
+
import { createHmac as createHmac3 } from "node:crypto";
|
|
101381
|
+
var BLOB_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
101382
|
+
var BLOB_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
101383
|
+
var MAX_KEY_LENGTH = 1024;
|
|
101384
|
+
var SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
101385
|
+
function configError(reason) {
|
|
101386
|
+
return new McpToolError(`MCP_BLOB_BASE_URL is not usable: ${reason}.`, {
|
|
101387
|
+
hint: "It should look like https://<host>/b/<registrationId> \u2014 check the deployment's environment.",
|
|
101388
|
+
kind: "unknown"
|
|
101389
|
+
});
|
|
101390
|
+
}
|
|
101391
|
+
function parseBase(baseUrl) {
|
|
101385
101392
|
let parsed;
|
|
101386
101393
|
try {
|
|
101387
101394
|
parsed = new URL(baseUrl);
|
|
101388
101395
|
} catch {
|
|
101389
|
-
throw
|
|
101390
|
-
}
|
|
101396
|
+
throw configError("not a valid URL");
|
|
101397
|
+
}
|
|
101398
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
101399
|
+
throw configError("scheme must be http(s)");
|
|
101400
|
+
if (parsed.username || parsed.password)
|
|
101401
|
+
throw configError("must not contain credentials");
|
|
101402
|
+
if (parsed.search || parsed.hash)
|
|
101403
|
+
throw configError("must not contain a query or fragment");
|
|
101391
101404
|
const path = parsed.pathname.replace(/^\/+|\/+$/g, "");
|
|
101392
101405
|
const prefix = path.startsWith("b/") ? path.slice(2) : path;
|
|
101393
|
-
|
|
101394
|
-
|
|
101395
|
-
|
|
101396
|
-
|
|
101397
|
-
|
|
101398
|
-
}
|
|
101399
|
-
|
|
101400
|
-
|
|
101401
|
-
|
|
101402
|
-
|
|
101403
|
-
|
|
101404
|
-
${
|
|
101406
|
+
if (!prefix || prefix === "b")
|
|
101407
|
+
throw configError("has no registration path");
|
|
101408
|
+
return { base: `${parsed.origin}/${path}`, prefix };
|
|
101409
|
+
}
|
|
101410
|
+
function badInput(message) {
|
|
101411
|
+
return new McpToolError(message, { kind: "unknown" });
|
|
101412
|
+
}
|
|
101413
|
+
function encodeKey(key) {
|
|
101414
|
+
if (typeof key !== "string" || key.length === 0)
|
|
101415
|
+
throw badInput("Blob key must be a non-empty string.");
|
|
101416
|
+
if (key.length > MAX_KEY_LENGTH)
|
|
101417
|
+
throw badInput(`Blob key is longer than ${MAX_KEY_LENGTH} characters.`);
|
|
101418
|
+
const segments = key.split("/");
|
|
101419
|
+
for (const seg of segments) {
|
|
101420
|
+
if (seg === "" || seg === "." || seg === ".." || !SAFE_SEGMENT.test(seg)) {
|
|
101421
|
+
throw badInput('Blob key must be relative, slash-separated segments of [A-Za-z0-9._~-] with no empty, "." or ".." segment.');
|
|
101422
|
+
}
|
|
101423
|
+
}
|
|
101424
|
+
return segments.map(encodeURIComponent).join("/");
|
|
101425
|
+
}
|
|
101426
|
+
function checkContentType(contentType) {
|
|
101427
|
+
if (typeof contentType !== "string" || contentType.length === 0 || /[\x00-\x1f\x7f]/.test(contentType)) {
|
|
101428
|
+
throw badInput("Blob content type must be a non-empty string without control characters.");
|
|
101429
|
+
}
|
|
101430
|
+
}
|
|
101431
|
+
function expiry(opts) {
|
|
101432
|
+
const ttl = opts?.ttlMs ?? BLOB_DEFAULT_TTL_MS;
|
|
101433
|
+
if (!Number.isFinite(ttl) || ttl <= 0 || ttl > BLOB_MAX_TTL_MS) {
|
|
101434
|
+
throw badInput(`Blob link TTL must be between 1 ms and ${BLOB_MAX_TTL_MS} ms (24h); the gateway refuses longer.`);
|
|
101435
|
+
}
|
|
101436
|
+
return Math.floor((opts?.now ?? Date.now()) + ttl);
|
|
101437
|
+
}
|
|
101438
|
+
function toArrayBuffer(view) {
|
|
101439
|
+
return view.slice().buffer;
|
|
101440
|
+
}
|
|
101441
|
+
function createBlobStore(opts) {
|
|
101442
|
+
const { base, prefix } = parseBase(opts.baseUrl);
|
|
101443
|
+
const signingKey = opts.signingKey;
|
|
101444
|
+
const sign2 = (payload) => createHmac3("sha256", signingKey).update(payload).digest("base64url");
|
|
101445
|
+
const link = (encodedKey, exp, sig) => `${base}/${encodedKey}?exp=${exp}&sig=${encodeURIComponent(sig)}`;
|
|
101446
|
+
const signGetUrl = (key, o2) => {
|
|
101447
|
+
const encoded = encodeKey(key);
|
|
101448
|
+
const exp = expiry(o2);
|
|
101449
|
+
return { url: link(encoded, exp, sign2(`${prefix}/${key}
|
|
101450
|
+
${exp}`)), expiresAtMs: exp };
|
|
101451
|
+
};
|
|
101452
|
+
const signPutUrl = (key, contentType, o2) => {
|
|
101453
|
+
const encoded = encodeKey(key);
|
|
101454
|
+
checkContentType(contentType);
|
|
101455
|
+
const exp = expiry(o2);
|
|
101456
|
+
return { url: link(encoded, exp, sign2(`put\0${prefix}/${key}\0${contentType}\0${exp}`)), expiresAtMs: exp };
|
|
101457
|
+
};
|
|
101458
|
+
const put = async (key, body, contentType, o2 = {}) => {
|
|
101459
|
+
const { url: url4 } = signPutUrl(key, contentType, o2);
|
|
101460
|
+
const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
101461
|
+
const doFetch = o2.fetchImpl ?? fetch;
|
|
101462
|
+
let res;
|
|
101463
|
+
try {
|
|
101464
|
+
res = await doFetch(url4, {
|
|
101465
|
+
method: "PUT",
|
|
101466
|
+
headers: { "content-type": contentType },
|
|
101467
|
+
body: toArrayBuffer(bytes),
|
|
101468
|
+
...o2.signal ? { signal: o2.signal } : {}
|
|
101469
|
+
});
|
|
101470
|
+
} catch (cause) {
|
|
101471
|
+
if (o2.signal?.aborted)
|
|
101472
|
+
throw cause;
|
|
101473
|
+
throw new McpToolError(`Blob store upload of ${key} did not complete (network error).`, {
|
|
101474
|
+
kind: "transport"
|
|
101475
|
+
});
|
|
101476
|
+
}
|
|
101477
|
+
if (!res.ok) {
|
|
101478
|
+
throw new McpToolError(`Blob store upload of ${key} failed (HTTP ${res.status}).`, {
|
|
101479
|
+
kind: "http",
|
|
101480
|
+
status: res.status
|
|
101481
|
+
});
|
|
101482
|
+
}
|
|
101483
|
+
return { key, bytes: bytes.byteLength };
|
|
101484
|
+
};
|
|
101485
|
+
return { baseUrl: base, prefix, signGetUrl, signPutUrl, put };
|
|
101405
101486
|
}
|
|
101406
|
-
|
|
101407
|
-
|
|
101408
|
-
const
|
|
101409
|
-
|
|
101410
|
-
|
|
101487
|
+
function blobStoreFromEnv(env = process.env) {
|
|
101488
|
+
const baseUrl = readEnvVar("MCP_BLOB_BASE_URL", { env });
|
|
101489
|
+
const signingKey = readEnvVar("MCP_BLOB_SIGNING_KEY", { env });
|
|
101490
|
+
if (!baseUrl || !signingKey)
|
|
101491
|
+
return void 0;
|
|
101492
|
+
return createBlobStore({ baseUrl, signingKey });
|
|
101411
101493
|
}
|
|
101412
101494
|
|
|
101495
|
+
// node_modules/@chrischall/mcp-utils/dist/export/zip.js
|
|
101496
|
+
var CRC_TABLE = (() => {
|
|
101497
|
+
const table = new Uint32Array(256);
|
|
101498
|
+
for (let n2 = 0; n2 < 256; n2++) {
|
|
101499
|
+
let c2 = n2;
|
|
101500
|
+
for (let k2 = 0; k2 < 8; k2++)
|
|
101501
|
+
c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
|
|
101502
|
+
table[n2] = c2 >>> 0;
|
|
101503
|
+
}
|
|
101504
|
+
return table;
|
|
101505
|
+
})();
|
|
101506
|
+
|
|
101413
101507
|
// src/tools/_receipt.ts
|
|
101414
101508
|
var MAX_RECEIPT_BYTES = 10 * 1024 * 1024;
|
|
101415
101509
|
var LIMIT_LABEL = "10 MiB";
|
|
@@ -101485,17 +101579,23 @@ async function readSandboxed(requested) {
|
|
|
101485
101579
|
await handle.close();
|
|
101486
101580
|
}
|
|
101487
101581
|
}
|
|
101582
|
+
var UPLOAD_ID_PATTERN = /^up\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[A-Za-z0-9_-][A-Za-z0-9._-]{0,99}$/;
|
|
101583
|
+
function safeKeyName(filename) {
|
|
101584
|
+
const base = filename.split(/[\\/]/).pop() ?? "";
|
|
101585
|
+
const cleaned = base.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._]+/, "").slice(0, 100);
|
|
101586
|
+
return cleaned || "receipt";
|
|
101587
|
+
}
|
|
101488
101588
|
var UPLOAD_READ_TTL_MS = 5 * 60 * 1e3;
|
|
101489
101589
|
async function readUpload(uploadId, client2) {
|
|
101490
101590
|
if (!UPLOAD_ID_PATTERN.test(uploadId)) {
|
|
101491
101591
|
throw new Error("receipt.upload_id is not one sw_get_upload_url issued (expected up/<uuid>/<filename>)");
|
|
101492
101592
|
}
|
|
101493
|
-
const
|
|
101494
|
-
if (!
|
|
101593
|
+
const store = blobStoreFromEnv();
|
|
101594
|
+
if (!store || !client2) {
|
|
101495
101595
|
throw new Error("receipt.upload_id only works on the hosted server; pass receipt.path or receipt.base64 here.");
|
|
101496
101596
|
}
|
|
101497
101597
|
try {
|
|
101498
|
-
const asset = await client2.fetchAsset(
|
|
101598
|
+
const asset = await client2.fetchAsset(store.signGetUrl(uploadId, { ttlMs: UPLOAD_READ_TTL_MS }).url, {
|
|
101499
101599
|
maxBytes: MAX_RECEIPT_BYTES
|
|
101500
101600
|
});
|
|
101501
101601
|
return Uint8Array.from(asset.bytes);
|
|
@@ -102319,12 +102419,12 @@ function registerReceiptTools(server, client2) {
|
|
|
102319
102419
|
const fallback = wanted === "original" ? "large" : "original";
|
|
102320
102420
|
const expense = await client2.request("GET", `/get_expense/${id}`);
|
|
102321
102421
|
const receipt = expense?.expense?.receipt;
|
|
102322
|
-
const
|
|
102323
|
-
if (!
|
|
102422
|
+
const url4 = receipt?.[wanted] || receipt?.[fallback];
|
|
102423
|
+
if (!url4) {
|
|
102324
102424
|
throw new Error(`Splitwise expense ${id} has no receipt attached.`);
|
|
102325
102425
|
}
|
|
102326
102426
|
const served = receipt?.[wanted] ? wanted : fallback;
|
|
102327
|
-
const asset = await client2.fetchAsset(
|
|
102427
|
+
const asset = await client2.fetchAsset(url4, { maxBytes: MAX_RECEIPT_BYTES2 });
|
|
102328
102428
|
if (asset.bytes.length === 0) {
|
|
102329
102429
|
throw new Error(`Splitwise returned an empty receipt body for expense ${id}.`);
|
|
102330
102430
|
}
|
|
@@ -102345,7 +102445,7 @@ function registerReceiptTools(server, client2) {
|
|
|
102345
102445
|
baseName: `splitwise-receipt-${id}`,
|
|
102346
102446
|
base64: base643,
|
|
102347
102447
|
// The MIME type, else the receipt URL's own extension, else `bin`.
|
|
102348
|
-
extension: extensionForMime(mimeType, { url:
|
|
102448
|
+
extension: extensionForMime(mimeType, { url: url4 }),
|
|
102349
102449
|
mode: RECEIPT_FILE_MODE
|
|
102350
102450
|
});
|
|
102351
102451
|
} catch (err) {
|
|
@@ -102411,7 +102511,7 @@ function registerReceiptTools(server, client2) {
|
|
|
102411
102511
|
bytes: asset.bytes.length,
|
|
102412
102512
|
content_type: mimeType ?? "unknown",
|
|
102413
102513
|
// The receipt URL itself is a signed capability, so report only its host.
|
|
102414
|
-
source_host: new URL(
|
|
102514
|
+
source_host: new URL(url4).host,
|
|
102415
102515
|
inline: inlined,
|
|
102416
102516
|
...inline === true && oversized ? {
|
|
102417
102517
|
// The write outcome is already known, so don't advise `write:true`
|
|
@@ -102448,16 +102548,15 @@ function registerUploadTools(server, _client) {
|
|
|
102448
102548
|
})
|
|
102449
102549
|
},
|
|
102450
102550
|
async ({ filename, content_type }) => {
|
|
102451
|
-
const
|
|
102452
|
-
if (!
|
|
102551
|
+
const store = blobStoreFromEnv();
|
|
102552
|
+
if (!store) {
|
|
102453
102553
|
throw new Error(
|
|
102454
102554
|
"sw_get_upload_url only works on the hosted server (no blob store is configured here). Pass the file as receipt.path (a file inside SPLITWISE_INPUT_DIR) or receipt.base64 instead."
|
|
102455
102555
|
);
|
|
102456
102556
|
}
|
|
102457
102557
|
const contentType = canonicalReceiptMime(content_type);
|
|
102458
102558
|
const key = `up/${randomUUID()}/${safeKeyName(filename)}`;
|
|
102459
|
-
const exp =
|
|
102460
|
-
const uploadUrl = signedPutUrl(cfg, key, contentType, exp);
|
|
102559
|
+
const { url: uploadUrl, expiresAtMs: exp } = store.signPutUrl(key, contentType, { ttlMs: UPLOAD_URL_TTL_MS });
|
|
102461
102560
|
return minifiedResult({
|
|
102462
102561
|
upload_id: key,
|
|
102463
102562
|
upload_url: uploadUrl,
|
|
@@ -102586,7 +102685,7 @@ function registerUtilityTools(server, client2) {
|
|
|
102586
102685
|
// src/index.ts
|
|
102587
102686
|
await runMcp({
|
|
102588
102687
|
name: "splitwise-mcp",
|
|
102589
|
-
version: "3.5.
|
|
102688
|
+
version: "3.5.1",
|
|
102590
102689
|
// x-release-please-version
|
|
102591
102690
|
deps: client,
|
|
102592
102691
|
banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { registerUtilityTools } from './tools/utilities.js';
|
|
|
21
21
|
// client into the same registrars instead of this singleton.
|
|
22
22
|
await runMcp({
|
|
23
23
|
name: 'splitwise-mcp',
|
|
24
|
-
version: '3.5.
|
|
24
|
+
version: '3.5.1', // x-release-please-version
|
|
25
25
|
deps: client,
|
|
26
26
|
banner: '[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
|
|
27
27
|
tools: [
|
package/dist/tools/_receipt.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { ApiError, readEnvVar } from '@chrischall/mcp-utils';
|
|
3
|
-
import {
|
|
3
|
+
import { blobStoreFromEnv } from '@chrischall/mcp-utils/export';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
/**
|
|
6
6
|
* Splitwise does not document an upload cap, so this is a conservative 10 MiB:
|
|
@@ -112,6 +112,18 @@ async function readSandboxed(requested) {
|
|
|
112
112
|
await handle.close();
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The only keys `receipt.upload_id` may name: `up/<uuid>/<safe name>`, exactly
|
|
117
|
+
* what `sw_get_upload_url` mints. Anything else in this registration's space
|
|
118
|
+
* is not the caller's to read through a receipt.
|
|
119
|
+
*/
|
|
120
|
+
export const UPLOAD_ID_PATTERN = /^up\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[A-Za-z0-9_-][A-Za-z0-9._-]{0,99}$/;
|
|
121
|
+
/** A filename made safe for a blob key segment: no separators, no dot-dot, no surprises. */
|
|
122
|
+
export function safeKeyName(filename) {
|
|
123
|
+
const base = filename.split(/[\\/]/).pop() ?? '';
|
|
124
|
+
const cleaned = base.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^[._]+/, '').slice(0, 100);
|
|
125
|
+
return cleaned || 'receipt';
|
|
126
|
+
}
|
|
115
127
|
/** How long the server's own read link for an upload lives — it is used at once. */
|
|
116
128
|
const UPLOAD_READ_TTL_MS = 5 * 60 * 1000;
|
|
117
129
|
/**
|
|
@@ -122,12 +134,12 @@ async function readUpload(uploadId, client) {
|
|
|
122
134
|
if (!UPLOAD_ID_PATTERN.test(uploadId)) {
|
|
123
135
|
throw new Error('receipt.upload_id is not one sw_get_upload_url issued (expected up/<uuid>/<filename>)');
|
|
124
136
|
}
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
137
|
+
const store = blobStoreFromEnv();
|
|
138
|
+
if (!store || !client) {
|
|
127
139
|
throw new Error('receipt.upload_id only works on the hosted server; pass receipt.path or receipt.base64 here.');
|
|
128
140
|
}
|
|
129
141
|
try {
|
|
130
|
-
const asset = await client.fetchAsset(
|
|
142
|
+
const asset = await client.fetchAsset(store.signGetUrl(uploadId, { ttlMs: UPLOAD_READ_TTL_MS }).url, {
|
|
131
143
|
maxBytes: MAX_RECEIPT_BYTES,
|
|
132
144
|
});
|
|
133
145
|
return Uint8Array.from(asset.bytes);
|
package/dist/tools/uploads.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { minifiedResult } from '@chrischall/mcp-utils';
|
|
4
|
-
import {
|
|
5
|
-
import { canonicalReceiptMime, MAX_RECEIPT_BYTES } from './_receipt.js';
|
|
4
|
+
import { blobStoreFromEnv } from '@chrischall/mcp-utils/export';
|
|
5
|
+
import { canonicalReceiptMime, MAX_RECEIPT_BYTES, safeKeyName } from './_receipt.js';
|
|
6
6
|
/** Long enough to run a curl in the same turn, short enough that a leaked link is soon dead. */
|
|
7
7
|
export const UPLOAD_URL_TTL_MS = 10 * 60 * 1000;
|
|
8
8
|
/**
|
|
@@ -24,14 +24,13 @@ export function registerUploadTools(server, _client) {
|
|
|
24
24
|
.describe('The file\'s type: image/png, image/jpeg, image/gif or application/pdf. The upload must send exactly this Content-Type.'),
|
|
25
25
|
}),
|
|
26
26
|
}, async ({ filename, content_type }) => {
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
27
|
+
const store = blobStoreFromEnv();
|
|
28
|
+
if (!store) {
|
|
29
29
|
throw new Error('sw_get_upload_url only works on the hosted server (no blob store is configured here). Pass the file as receipt.path (a file inside SPLITWISE_INPUT_DIR) or receipt.base64 instead.');
|
|
30
30
|
}
|
|
31
31
|
const contentType = canonicalReceiptMime(content_type);
|
|
32
32
|
const key = `up/${randomUUID()}/${safeKeyName(filename)}`;
|
|
33
|
-
const exp =
|
|
34
|
-
const uploadUrl = signedPutUrl(cfg, key, contentType, exp);
|
|
33
|
+
const { url: uploadUrl, expiresAtMs: exp } = store.signPutUrl(key, contentType, { ttlMs: UPLOAD_URL_TTL_MS });
|
|
35
34
|
return minifiedResult({
|
|
36
35
|
upload_id: key,
|
|
37
36
|
upload_url: uploadUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "splitwise-mcp",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/splitwise-mcp",
|
|
5
5
|
"description": "Splitwise MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^3.
|
|
48
|
+
"@chrischall/mcp-utils": "^3.3.0",
|
|
49
49
|
"@modelcontextprotocol/server": "^2.3.0",
|
|
50
50
|
"dotenv": "^18.0.2",
|
|
51
51
|
"unpdf": "^1.8.1",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/splitwise-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "3.5.
|
|
9
|
+
"version": "3.5.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "splitwise-mcp",
|
|
14
|
-
"version": "3.5.
|
|
14
|
+
"version": "3.5.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/dist/tools/_blob.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { createHmac } from 'crypto';
|
|
2
|
-
import { readEnvVar } from '@chrischall/mcp-utils';
|
|
3
|
-
export function blobConfig() {
|
|
4
|
-
const baseUrl = readEnvVar('MCP_BLOB_BASE_URL');
|
|
5
|
-
const signingKey = readEnvVar('MCP_BLOB_SIGNING_KEY');
|
|
6
|
-
if (!baseUrl || !signingKey)
|
|
7
|
-
return undefined;
|
|
8
|
-
let parsed;
|
|
9
|
-
try {
|
|
10
|
-
parsed = new URL(baseUrl);
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
throw new Error(`MCP_BLOB_BASE_URL is not a valid URL — it should look like https://<host>/b/<registrationId>`);
|
|
14
|
-
}
|
|
15
|
-
const path = parsed.pathname.replace(/^\/+|\/+$/g, '');
|
|
16
|
-
const prefix = path.startsWith('b/') ? path.slice(2) : path;
|
|
17
|
-
return { base: baseUrl.replace(/\/+$/, ''), prefix, signingKey };
|
|
18
|
-
}
|
|
19
|
-
const sign = (cfg, payload) => createHmac('sha256', cfg.signingKey).update(payload).digest('base64url');
|
|
20
|
-
function url(cfg, key, exp, sig) {
|
|
21
|
-
return `${cfg.base}/${key}?exp=${exp}&sig=${encodeURIComponent(sig)}`;
|
|
22
|
-
}
|
|
23
|
-
export function signedPutUrl(cfg, key, contentType, exp) {
|
|
24
|
-
return url(cfg, key, exp, sign(cfg, `put\0${cfg.prefix}/${key}\0${contentType}\0${exp}`));
|
|
25
|
-
}
|
|
26
|
-
export function signedGetUrl(cfg, key, exp) {
|
|
27
|
-
return url(cfg, key, exp, sign(cfg, `${cfg.prefix}/${key}\n${exp}`));
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* The only keys `receipt.upload_id` may name: `up/<uuid>/<safe name>`, exactly
|
|
31
|
-
* what `sw_get_upload_url` mints. Anything else in this registration's space
|
|
32
|
-
* is not the caller's to read through a receipt.
|
|
33
|
-
*/
|
|
34
|
-
export const UPLOAD_ID_PATTERN = /^up\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[A-Za-z0-9_-][A-Za-z0-9._-]{0,99}$/;
|
|
35
|
-
/** A filename made safe for a key segment: no separators, no dot-dot, no surprises. */
|
|
36
|
-
export function safeKeyName(filename) {
|
|
37
|
-
const base = filename.split(/[\\/]/).pop() ?? '';
|
|
38
|
-
const cleaned = base.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^[._]+/, '').slice(0, 100);
|
|
39
|
-
return cleaned || 'receipt';
|
|
40
|
-
}
|