splitwise-mcp 2.1.4 → 2.1.6

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.
@@ -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": "2.1.4"
10
+ "version": "2.1.6"
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": "2.1.4",
18
+ "version": "2.1.6",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "splitwise-mcp",
3
3
  "displayName": "Splitwise",
4
- "version": "2.1.4",
4
+ "version": "2.1.6",
5
5
  "description": "MCP server for Splitwise — natural-language expense and group management via the Splitwise API",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/bundle.js CHANGED
@@ -3644,7 +3644,12 @@ var require_fast_uri = __commonJS({
3644
3644
  }
3645
3645
  function resolve(baseURI, relativeURI, options) {
3646
3646
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3647
- const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3647
+ const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3648
+ const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
3649
+ if (baseMalformed || relativeMalformed) {
3650
+ throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
3651
+ }
3652
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
3648
3653
  schemelessOptions.skipEscape = true;
3649
3654
  return serialize(resolved, schemelessOptions);
3650
3655
  }
@@ -3770,6 +3775,7 @@ var require_fast_uri = __commonJS({
3770
3775
  }
3771
3776
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3772
3777
  var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
3778
+ var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
3773
3779
  function getParseError(parsed, matches) {
3774
3780
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3775
3781
  return 'URI path must start with "/" when authority is present.';
@@ -3804,6 +3810,20 @@ var require_fast_uri = __commonJS({
3804
3810
  parsed.error = "URI authority must not contain a literal backslash.";
3805
3811
  malformedAuthorityOrPort = true;
3806
3812
  }
3813
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
3814
+ if (introducerMatch !== null) {
3815
+ const region = introducerMatch[1];
3816
+ const normalizedRegion = region.replace(/[\t\n\r]/g, "");
3817
+ if (normalizedRegion.length >= 2) {
3818
+ if (normalizedRegion.slice(0, 2) !== "//") {
3819
+ parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
3820
+ malformedAuthorityOrPort = true;
3821
+ } else if (region.length !== normalizedRegion.length) {
3822
+ parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
3823
+ malformedAuthorityOrPort = true;
3824
+ }
3825
+ }
3826
+ }
3807
3827
  const matches = uri.match(URI_PARSE);
3808
3828
  if (matches) {
3809
3829
  parsed.scheme = matches[1];
@@ -23048,17 +23068,33 @@ function normalizeObjectSchema(schema) {
23048
23068
  }
23049
23069
  return void 0;
23050
23070
  }
23071
+ function getDotPath(path) {
23072
+ if (path.length === 0) {
23073
+ return "object root";
23074
+ }
23075
+ return path.reduce((acc, seg, index) => {
23076
+ if (index === 0) {
23077
+ return String(seg);
23078
+ }
23079
+ if (typeof seg === "number") {
23080
+ return `${acc}[${seg}]`;
23081
+ }
23082
+ return `${acc}.${seg}`;
23083
+ }, "");
23084
+ }
23051
23085
  function getParseErrorMessage(error51) {
23052
23086
  if (error51 && typeof error51 === "object") {
23087
+ if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
23088
+ return error51.issues.map((i) => {
23089
+ if (!i.path?.length) {
23090
+ return i.message;
23091
+ }
23092
+ return `${i.message} at ${getDotPath(i.path)}`;
23093
+ }).join("\n");
23094
+ }
23053
23095
  if ("message" in error51 && typeof error51.message === "string") {
23054
23096
  return error51.message;
23055
23097
  }
23056
- if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
23057
- const firstIssue = error51.issues[0];
23058
- if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
23059
- return String(firstIssue.message);
23060
- }
23061
- }
23062
23098
  try {
23063
23099
  return JSON.stringify(error51);
23064
23100
  } catch {
@@ -29673,16 +29709,7 @@ var Server = class extends Protocol {
29673
29709
  if (!methodSchema) {
29674
29710
  throw new Error("Schema is missing a method literal");
29675
29711
  }
29676
- let methodValue;
29677
- if (isZ4Schema(methodSchema)) {
29678
- const v4Schema = methodSchema;
29679
- const v4Def = v4Schema._zod?.def;
29680
- methodValue = v4Def?.value ?? v4Schema.value;
29681
- } else {
29682
- const v3Schema = methodSchema;
29683
- const legacyDef = v3Schema._def;
29684
- methodValue = legacyDef?.value ?? v3Schema.value;
29685
- }
29712
+ const methodValue = getLiteralValue(methodSchema);
29686
29713
  if (typeof methodValue !== "string") {
29687
29714
  throw new Error("Schema method literal must be a string");
29688
29715
  }
@@ -30870,8 +30897,17 @@ var EMPTY_COMPLETION_RESULT = {
30870
30897
  import process3 from "node:process";
30871
30898
 
30872
30899
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
30900
+ var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
30873
30901
  var ReadBuffer = class {
30902
+ constructor(options) {
30903
+ this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
30904
+ }
30874
30905
  append(chunk) {
30906
+ const newSize = (this._buffer?.length ?? 0) + chunk.length;
30907
+ if (newSize > this._maxBufferSize) {
30908
+ this.clear();
30909
+ throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
30910
+ }
30875
30911
  this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
30876
30912
  }
30877
30913
  readMessage() {
@@ -30899,18 +30935,24 @@ function serializeMessage(message) {
30899
30935
 
30900
30936
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
30901
30937
  var StdioServerTransport = class {
30902
- constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
30938
+ constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
30903
30939
  this._stdin = _stdin;
30904
30940
  this._stdout = _stdout;
30905
- this._readBuffer = new ReadBuffer();
30906
30941
  this._started = false;
30907
30942
  this._ondata = (chunk) => {
30908
- this._readBuffer.append(chunk);
30909
- this.processReadBuffer();
30943
+ try {
30944
+ this._readBuffer.append(chunk);
30945
+ this.processReadBuffer();
30946
+ } catch (error51) {
30947
+ this.onerror?.(error51);
30948
+ this.close().catch(() => {
30949
+ });
30950
+ }
30910
30951
  };
30911
30952
  this._onerror = (error51) => {
30912
30953
  this.onerror?.(error51);
30913
30954
  };
30955
+ this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
30914
30956
  }
30915
30957
  /**
30916
30958
  * Starts listening for messages on stdin.
@@ -31304,10 +31346,10 @@ var SplitwiseClient = class {
31304
31346
  * host's install-time smoke test) when SPLITWISE_API_KEY isn't set yet.
31305
31347
  * Tool calls re-raise the error at request time.
31306
31348
  *
31307
- * Optional constructor seam: a hosted per-user deployment (e.g. a Cloudflare
31308
- * Worker "remote connector") builds one client per request with that user's
31309
- * `apiKey` injected. The stdio path passes no options, so the key resolves
31310
- * from the environment exactly as before — byte-for-byte identical behaviour.
31349
+ * Optional constructor seam: a hosted per-user deployment builds one client
31350
+ * per request with that user's `apiKey` injected. The stdio path passes no
31351
+ * options, so the key resolves from the environment exactly as before —
31352
+ * byte-for-byte identical behaviour.
31311
31353
  */
31312
31354
  constructor(opts) {
31313
31355
  const key = opts?.apiKey ?? readEnvVar("SPLITWISE_API_KEY");
@@ -31738,7 +31780,7 @@ function registerUtilityTools(server, client2) {
31738
31780
  // src/index.ts
31739
31781
  await runMcp({
31740
31782
  name: "splitwise-mcp",
31741
- version: "2.1.4",
31783
+ version: "2.1.6",
31742
31784
  // x-release-please-version
31743
31785
  deps: client,
31744
31786
  banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
package/dist/client.js CHANGED
@@ -4,15 +4,15 @@ import { loadDotenvSafely, readEnvVar, createApiClient } from '@chrischall/mcp-u
4
4
  // Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
5
5
  // bundle). `loadDotenvSafely` swallows a missing dotenv module and never lets
6
6
  // .env override a host-provided value.
7
- // The try/catch guards the Cloudflare Worker runtime, where `import.meta.url`
8
- // is undefined and `fileURLToPath(undefined)` would throw at module init
9
- // (Worker startup validation) — there is no filesystem / .env to load there.
7
+ // The try/catch guards a runtime where `import.meta.url` is undefined and
8
+ // `fileURLToPath(undefined)` would throw at module init — a sandboxed one has
9
+ // no filesystem or .env to load anyway.
10
10
  try {
11
11
  const dir = dirname(fileURLToPath(import.meta.url));
12
12
  await loadDotenvSafely({ path: join(dir, '..', '.env'), override: false });
13
13
  }
14
14
  catch {
15
- /* non-Node runtime (Workers): no .env to load */
15
+ /* non-Node runtime: no .env to load */
16
16
  }
17
17
  const BASE_URL = 'https://secure.splitwise.com/api/v3.0';
18
18
  const SERVICE_NAME = 'Splitwise';
@@ -25,10 +25,10 @@ export class SplitwiseClient {
25
25
  * host's install-time smoke test) when SPLITWISE_API_KEY isn't set yet.
26
26
  * Tool calls re-raise the error at request time.
27
27
  *
28
- * Optional constructor seam: a hosted per-user deployment (e.g. a Cloudflare
29
- * Worker "remote connector") builds one client per request with that user's
30
- * `apiKey` injected. The stdio path passes no options, so the key resolves
31
- * from the environment exactly as before — byte-for-byte identical behaviour.
28
+ * Optional constructor seam: a hosted per-user deployment builds one client
29
+ * per request with that user's `apiKey` injected. The stdio path passes no
30
+ * options, so the key resolves from the environment exactly as before —
31
+ * byte-for-byte identical behaviour.
32
32
  */
33
33
  constructor(opts) {
34
34
  // Injected apiKey (hosted per-user path) takes precedence; otherwise fall
package/dist/index.js CHANGED
@@ -13,11 +13,11 @@ import { registerUtilityTools } from './tools/utilities.js';
13
13
  // first request. That preserves the deferred-config-error pattern: the server
14
14
  // boots and answers the host's install-time tools/list smoke test even when
15
15
  // SPLITWISE_API_KEY is absent — the configuration error only surfaces on the
16
- // first tool call. A hosted per-user connector (a later task) injects its own
17
- // per-request client into the same registrars instead of this singleton.
16
+ // first tool call. A hosted per-user deployment injects its own per-request
17
+ // client into the same registrars instead of this singleton.
18
18
  await runMcp({
19
19
  name: 'splitwise-mcp',
20
- version: '2.1.4', // x-release-please-version
20
+ version: '2.1.6', // x-release-please-version
21
21
  deps: client,
22
22
  banner: '[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
23
23
  tools: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splitwise-mcp",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
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>",
@@ -40,28 +40,19 @@
40
40
  "dev": "node dist/index.js",
41
41
  "test": "vitest run",
42
42
  "test:watch": "vitest",
43
- "test:coverage": "vitest run --coverage",
44
- "worker:dev": "wrangler dev",
45
- "worker:deploy": "wrangler deploy",
46
- "worker:test": "vitest run --config vitest.workers.config.ts"
43
+ "test:coverage": "vitest run --coverage"
47
44
  },
48
45
  "dependencies": {
49
- "@chrischall/mcp-utils": "^0.13.0",
46
+ "@chrischall/mcp-utils": "^0.14.0",
50
47
  "@modelcontextprotocol/sdk": "^1.29.0",
51
48
  "dotenv": "^17.4.0",
52
49
  "zod": "^4.4.2"
53
50
  },
54
51
  "devDependencies": {
55
- "@chrischall/mcp-connector": "^1.1.1",
56
- "@cloudflare/vitest-pool-workers": "^0.18.4",
57
- "@cloudflare/workers-oauth-provider": "^0.8.1",
58
- "@cloudflare/workers-types": "^5.20260708.1",
59
52
  "@types/node": "^26.0.0",
60
53
  "@vitest/coverage-v8": "^4.1.2",
61
- "agents": "^0.19.0",
62
54
  "esbuild": "^0.28.0",
63
55
  "typescript": "^7.0.2",
64
- "vitest": "^4.1.2",
65
- "wrangler": "^4.110.0"
56
+ "vitest": "^4.1.2"
66
57
  }
67
58
  }
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": "2.1.4",
9
+ "version": "2.1.6",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "splitwise-mcp",
14
- "version": "2.1.4",
14
+ "version": "2.1.6",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -1,23 +0,0 @@
1
- import { SplitwiseClient } from './client.js';
2
- /**
3
- * `ConnectorAuth` for the Splitwise remote connector: the login page collects
4
- * the user's own Splitwise API key (from https://secure.splitwise.com/apps/register),
5
- * verifies it by constructing a `SplitwiseClient` with the key and calling the
6
- * current-user endpoint (a bad key throws, which the connector surfaces back on
7
- * the login page), and stores `{ apiKey }` as the OAuth props that `worker.ts`'s
8
- * `buildClient` turns into a per-user client.
9
- */
10
- export const splitwiseAuth = {
11
- service: 'Splitwise',
12
- accent: '#1CC29F',
13
- privacyNote: 'Your Splitwise API key is stored encrypted and used only to call Splitwise on your behalf.',
14
- fields: [{ name: 'apiKey', label: 'Splitwise API key', type: 'password' }],
15
- async login(fields) {
16
- // Verify the key up front — an invalid key makes /get_current_user throw
17
- // here, which the connector surfaces back on the login page. We discard the
18
- // response: the per-user client is built fresh from the stored key.
19
- const client = new SplitwiseClient({ apiKey: fields.apiKey });
20
- await client.request('GET', '/get_current_user');
21
- return { apiKey: fields.apiKey };
22
- },
23
- };