ofw-mcp 2.3.0 → 2.3.2
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/auth.js +9 -16
- package/dist/bundle.js +814 -224
- package/dist/cache.js +20 -1
- package/dist/client.js +73 -59
- package/dist/config.js +6 -4
- package/dist/index.js +20 -11
- package/dist/tools/_shared.js +9 -12
- package/dist/tools/messages.js +6 -4
- package/package.json +4 -3
- package/server.json +2 -2
package/dist/cache.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { getCacheDbPath } from './config.js';
|
|
5
5
|
let instance = null;
|
|
@@ -62,15 +62,33 @@ function migrate(db) {
|
|
|
62
62
|
db.exec(SCHEMA_V2);
|
|
63
63
|
db.prepare('INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run('schema_version', '2');
|
|
64
64
|
}
|
|
65
|
+
// The cache holds full co-parenting message history — keep it private to the
|
|
66
|
+
// owning user. Modes are asserted on every open (not just creation): mkdirSync
|
|
67
|
+
// `mode` and SQLite's default file mode only apply when the path is first
|
|
68
|
+
// created, so a pre-existing dir/db keeps whatever (world-readable) mode it
|
|
69
|
+
// had. The -wal/-shm siblings appear and disappear with WAL checkpoints, hence
|
|
70
|
+
// the existence check.
|
|
71
|
+
function enforceCachePermissions(dbPath) {
|
|
72
|
+
chmodSync(dirname(dbPath), 0o700);
|
|
73
|
+
chmodSync(dbPath, 0o600);
|
|
74
|
+
for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
75
|
+
if (existsSync(sibling))
|
|
76
|
+
chmodSync(sibling, 0o600);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
65
79
|
export function openCache() {
|
|
66
80
|
if (instance)
|
|
67
81
|
return instance;
|
|
68
82
|
const path = getCacheDbPath();
|
|
69
83
|
mkdirSync(dirname(path), { recursive: true });
|
|
70
84
|
const db = new DatabaseSync(path);
|
|
85
|
+
// First pass: lock down dir + db before WAL siblings exist.
|
|
86
|
+
enforceCachePermissions(path);
|
|
71
87
|
db.exec('PRAGMA journal_mode = WAL');
|
|
72
88
|
db.exec('PRAGMA foreign_keys = ON');
|
|
73
89
|
migrate(db);
|
|
90
|
+
// Second pass: the migration writes created -wal/-shm — lock those down too.
|
|
91
|
+
enforceCachePermissions(path);
|
|
74
92
|
instance = { db };
|
|
75
93
|
return instance;
|
|
76
94
|
}
|
|
@@ -182,6 +200,7 @@ export function countMessages(opts) {
|
|
|
182
200
|
const { where, params } = buildMessageFilter(opts);
|
|
183
201
|
const r = db.prepare(`SELECT COUNT(*) as n FROM messages ${where}`)
|
|
184
202
|
.get(...params);
|
|
203
|
+
/* v8 ignore next -- SELECT COUNT(*) always returns exactly one row; the ?./?? are defensive */
|
|
185
204
|
return r?.n ?? 0;
|
|
186
205
|
}
|
|
187
206
|
function draftFromDb(r) {
|
package/dist/client.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
+
import { loadDotenvSafely } from '@chrischall/mcp-utils';
|
|
2
|
+
import { TokenManager } from '@chrischall/mcp-utils/session';
|
|
1
3
|
import { dirname, join } from 'path';
|
|
2
4
|
import { fileURLToPath } from 'url';
|
|
3
5
|
import { resolveAuth } from './auth.js';
|
|
4
6
|
import { parseBoolEnv } from './config.js';
|
|
5
7
|
import { BASE_URL, OFW_PROTOCOL_HEADERS, OFW_TOKEN_TTL_MS, OFW_TOKEN_EXPIRY_SKEW_MS } from './protocol.js';
|
|
6
|
-
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
// not available — rely on process.env (mcpb sets credentials via mcp_config.env)
|
|
14
|
-
}
|
|
8
|
+
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
|
|
9
|
+
// bundle). loadDotenvSafely applies override:false + quiet:true and swallows a
|
|
10
|
+
// missing dotenv module, matching the prior inline try/catch exactly.
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
await loadDotenvSafely({ path: join(__dirname, '..', '.env') });
|
|
15
13
|
// Parse a Content-Disposition header for a filename. Prefers RFC 6266
|
|
16
14
|
// `filename*=UTF-8''…` (percent-decoded) and falls back to `filename="…"`.
|
|
17
15
|
function parseContentDispositionFilename(cd) {
|
|
@@ -36,6 +34,7 @@ function debugLogEnabled() {
|
|
|
36
34
|
}
|
|
37
35
|
function redactHeaders(h) {
|
|
38
36
|
const out = { ...h };
|
|
37
|
+
/* v8 ignore next -- request headers always carry Authorization (set in request()); the guard is defensive for arbitrary header maps */
|
|
39
38
|
if (out.Authorization)
|
|
40
39
|
out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}…`;
|
|
41
40
|
return out;
|
|
@@ -53,12 +52,44 @@ function getRequestTimeoutMs() {
|
|
|
53
52
|
const n = Number(raw.trim());
|
|
54
53
|
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
55
54
|
}
|
|
55
|
+
// Sentinel "refresh token" handed to the shared TokenManager. OFW has no
|
|
56
|
+
// OAuth-style refresh token — every renewal re-runs the full `resolveAuth()`
|
|
57
|
+
// (password POST or fetchproxy snapshot). The TokenManager only refuses to
|
|
58
|
+
// refresh when its refresh token is `undefined`, so a non-empty placeholder
|
|
59
|
+
// keeps the single-flight refresh path live; the refresh callback ignores it.
|
|
60
|
+
const OFW_REFRESH_SENTINEL = 'ofw';
|
|
56
61
|
export class OFWClient {
|
|
57
|
-
token
|
|
58
|
-
|
|
62
|
+
// Bearer-token lifecycle is delegated to the shared, race-safe TokenManager
|
|
63
|
+
// (proactive refresh inside the skew window, single-flight refresh so a burst
|
|
64
|
+
// of concurrent callers coalesces onto ONE `resolveAuth()`, and a 401-replay
|
|
65
|
+
// guarded against double-refresh). It is created lazily, seeded with an
|
|
66
|
+
// already-expired placeholder token so the first request drives the refresh
|
|
67
|
+
// callback — i.e. the original "log in on first request" behavior.
|
|
68
|
+
tokenManager;
|
|
69
|
+
getTokenManager() {
|
|
70
|
+
if (!this.tokenManager) {
|
|
71
|
+
this.tokenManager = new TokenManager({
|
|
72
|
+
initial: { accessToken: '', refreshToken: OFW_REFRESH_SENTINEL, expiresAt: 0 },
|
|
73
|
+
skewMs: OFW_TOKEN_EXPIRY_SKEW_MS,
|
|
74
|
+
// Map OFW's mint/refresh onto the refresh callback. `resolveAuth()`
|
|
75
|
+
// returns a token and a best-effort expiry; when the fetchproxy path
|
|
76
|
+
// can't supply one we fall back to the same 6h estimate the password
|
|
77
|
+
// path uses (the 401-replay covers a wrong guess). We re-arm the
|
|
78
|
+
// sentinel so the manager can refresh again later.
|
|
79
|
+
refresh: async () => {
|
|
80
|
+
const { token, expiresAt } = await resolveAuth();
|
|
81
|
+
return {
|
|
82
|
+
accessToken: token,
|
|
83
|
+
refreshToken: OFW_REFRESH_SENTINEL,
|
|
84
|
+
expiresAt: (expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS)).getTime(),
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return this.tokenManager;
|
|
90
|
+
}
|
|
59
91
|
async request(method, path, body) {
|
|
60
|
-
await this.
|
|
61
|
-
const response = await this.fetchWithRetry(method, path, body, 'application/json', false);
|
|
92
|
+
const response = await this.fetchAuthed(method, path, body, 'application/json');
|
|
62
93
|
const text = await response.text();
|
|
63
94
|
if (debugLogEnabled()) {
|
|
64
95
|
console.error(`[ofw-debug] response body: ${text || '<empty>'}`);
|
|
@@ -67,23 +98,45 @@ export class OFWClient {
|
|
|
67
98
|
}
|
|
68
99
|
/** Like `request`, but returns the raw bytes plus Content-Type/-Disposition metadata. */
|
|
69
100
|
async requestBinary(method, path) {
|
|
70
|
-
await this.
|
|
71
|
-
const response = await this.fetchWithRetry(method, path, undefined, 'application/octet-stream', false);
|
|
101
|
+
const response = await this.fetchAuthed(method, path, undefined, 'application/octet-stream');
|
|
72
102
|
return {
|
|
73
103
|
body: Buffer.from(await response.arrayBuffer()),
|
|
74
104
|
contentType: response.headers.get('content-type'),
|
|
75
105
|
suggestedFileName: parseContentDispositionFilename(response.headers.get('content-disposition') ?? ''),
|
|
76
106
|
};
|
|
77
107
|
}
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
108
|
+
// Authenticated fetch for both JSON and binary callers. Auth (proactive
|
|
109
|
+
// refresh inside the skew window + one 401-replay, guarded against a
|
|
110
|
+
// double-refresh under concurrency) is delegated to the shared TokenManager's
|
|
111
|
+
// `withAuth`. The 429 wait-and-replay and the non-2xx → throw remain here.
|
|
112
|
+
async fetchAuthed(method, path, body, accept) {
|
|
113
|
+
// `withAuth` invokes `call` once, and again after a refresh on a 401. The
|
|
114
|
+
// second invocation is the replay — mark it `(retry)` in the debug log,
|
|
115
|
+
// preserving the prior bespoke-loop diagnostic.
|
|
116
|
+
let attempt = 0;
|
|
117
|
+
let response = await this.getTokenManager().withAuth((token) => this.fetchOnce(method, path, body, accept, token, attempt++ > 0));
|
|
118
|
+
if (response.status === 429) {
|
|
119
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
120
|
+
response = await this.getTokenManager().withAuth((token) => this.fetchOnce(method, path, body, accept, token, true));
|
|
121
|
+
if (response.status === 429)
|
|
122
|
+
throw new Error('Rate limited by OFW API');
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
126
|
+
}
|
|
127
|
+
return response;
|
|
128
|
+
}
|
|
129
|
+
// A single OFW API fetch with the bearer token supplied by `withAuth`.
|
|
130
|
+
// Carries the per-request timeout (AbortController + setTimeout so vitest
|
|
131
|
+
// fake timers can drive it and we attach a clear error message) and the
|
|
132
|
+
// OFW_DEBUG_LOG instrumentation. Returns the raw Response — 401/429/non-2xx
|
|
133
|
+
// handling lives in the callers (`withAuth` and `fetchAuthed`).
|
|
134
|
+
async fetchOnce(method, path, body, accept, token, isRetry = false) {
|
|
82
135
|
const isFormData = body instanceof FormData;
|
|
83
136
|
const headers = {
|
|
84
137
|
...OFW_PROTOCOL_HEADERS,
|
|
85
138
|
Accept: accept,
|
|
86
|
-
Authorization: `Bearer ${
|
|
139
|
+
Authorization: `Bearer ${token}`,
|
|
87
140
|
};
|
|
88
141
|
if (body !== undefined && !isFormData)
|
|
89
142
|
headers['Content-Type'] = 'application/json';
|
|
@@ -133,46 +186,7 @@ export class OFWClient {
|
|
|
133
186
|
if (debugLogEnabled()) {
|
|
134
187
|
console.error(`[ofw-debug] ← ${response.status} ${response.statusText} (${Date.now() - startedAt}ms)`);
|
|
135
188
|
}
|
|
136
|
-
if (response.status === 401 && !isRetry) {
|
|
137
|
-
this.token = null;
|
|
138
|
-
this.tokenExpiry = null;
|
|
139
|
-
await this.ensureAuthenticated();
|
|
140
|
-
return this.fetchWithRetry(method, path, body, accept, true);
|
|
141
|
-
}
|
|
142
|
-
if (response.status === 429) {
|
|
143
|
-
if (!isRetry) {
|
|
144
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
145
|
-
return this.fetchWithRetry(method, path, body, accept, true);
|
|
146
|
-
}
|
|
147
|
-
throw new Error('Rate limited by OFW API');
|
|
148
|
-
}
|
|
149
|
-
if (!response.ok) {
|
|
150
|
-
throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
151
|
-
}
|
|
152
189
|
return response;
|
|
153
190
|
}
|
|
154
|
-
async ensureAuthenticated() {
|
|
155
|
-
if (!this.isTokenExpiredSoon())
|
|
156
|
-
return;
|
|
157
|
-
await this.login();
|
|
158
|
-
}
|
|
159
|
-
// Auth resolution is delegated to `./auth.ts`. This client doesn't care
|
|
160
|
-
// whether the token came from a password POST or from a one-shot
|
|
161
|
-
// fetchproxy session-snapshot — it just consumes the result.
|
|
162
|
-
//
|
|
163
|
-
// If `expiresAt` is missing (the fetchproxy path on a tab whose
|
|
164
|
-
// browser didn't persist tokenExpiry), we fall back to the same 6h
|
|
165
|
-
// estimate the password path uses. The 401-replay path covers us if
|
|
166
|
-
// the estimate is wrong.
|
|
167
|
-
async login() {
|
|
168
|
-
const { token, expiresAt } = await resolveAuth();
|
|
169
|
-
this.token = token;
|
|
170
|
-
this.tokenExpiry = expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS);
|
|
171
|
-
}
|
|
172
|
-
isTokenExpiredSoon() {
|
|
173
|
-
if (!this.token || !this.tokenExpiry)
|
|
174
|
-
return true;
|
|
175
|
-
return this.tokenExpiry.getTime() - Date.now() < OFW_TOKEN_EXPIRY_SKEW_MS;
|
|
176
|
-
}
|
|
177
191
|
}
|
|
178
192
|
export const client = new OFWClient();
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { parseBoolEnv as parseBoolEnvUtil } from '@chrischall/mcp-utils';
|
|
4
5
|
// Cache identity drives the per-user SQLite DB filename. Order of preference:
|
|
5
6
|
// 1. OFW_CACHE_IDENTITY — explicit override for users who want to label the
|
|
6
7
|
// cache themselves (e.g. when authing via fetchproxy and OFW_USERNAME is
|
|
@@ -45,12 +46,13 @@ export function getAttachmentsDir() {
|
|
|
45
46
|
* (case-insensitive, trimmed). Anything else — unset, empty, or other
|
|
46
47
|
* values — is false. Used for OFW_INLINE_ATTACHMENTS, OFW_DISABLE_FETCHPROXY,
|
|
47
48
|
* OFW_DEBUG_LOG, etc.
|
|
49
|
+
*
|
|
50
|
+
* Delegates to @chrischall/mcp-utils' `parseBoolEnv` (which also recognizes
|
|
51
|
+
* the falsy set 0/false/no/off — behavior-equivalent here since callers only
|
|
52
|
+
* care about the truthy case and everything else defaults to false).
|
|
48
53
|
*/
|
|
49
54
|
export function parseBoolEnv(name) {
|
|
50
|
-
|
|
51
|
-
if (typeof raw !== 'string')
|
|
52
|
-
return false;
|
|
53
|
-
return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase());
|
|
55
|
+
return parseBoolEnvUtil(name);
|
|
54
56
|
}
|
|
55
57
|
// Default for ofw_download_attachment's `inline` arg when the caller doesn't
|
|
56
58
|
// pass one. Set OFW_INLINE_ATTACHMENTS=true to have attachments returned as
|
package/dist/index.js
CHANGED
|
@@ -9,20 +9,29 @@ process.emit = function (event, ...args) {
|
|
|
9
9
|
}
|
|
10
10
|
return originalEmit(event, ...args);
|
|
11
11
|
};
|
|
12
|
-
import {
|
|
13
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { runMcp } from '@chrischall/mcp-utils';
|
|
14
13
|
import { client } from './client.js';
|
|
15
14
|
import { registerUserTools } from './tools/user.js';
|
|
16
15
|
import { registerMessageTools } from './tools/messages.js';
|
|
17
16
|
import { registerCalendarTools } from './tools/calendar.js';
|
|
18
17
|
import { registerExpenseTools } from './tools/expenses.js';
|
|
19
18
|
import { registerJournalTools } from './tools/journal.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
// runMcp builds the McpServer, applies the registrars (with `client` threaded
|
|
20
|
+
// through as deps), prints the banner to stderr, wires SIGINT/SIGTERM graceful
|
|
21
|
+
// shutdown, and connects the stdio transport. The deferred-config-error pattern
|
|
22
|
+
// is preserved: `client` is constructed at module load in ./client.js (auth is
|
|
23
|
+
// resolved lazily on the first tool call), so the host's initial tools/list
|
|
24
|
+
// always succeeds before any credential check runs.
|
|
25
|
+
await runMcp({
|
|
26
|
+
name: 'ofw',
|
|
27
|
+
version: '2.3.2', // x-release-please-version
|
|
28
|
+
deps: client,
|
|
29
|
+
tools: [
|
|
30
|
+
registerUserTools,
|
|
31
|
+
registerMessageTools,
|
|
32
|
+
registerCalendarTools,
|
|
33
|
+
registerExpenseTools,
|
|
34
|
+
registerJournalTools,
|
|
35
|
+
],
|
|
36
|
+
banner: '[ofw-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
|
|
37
|
+
});
|
package/dist/tools/_shared.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
1
|
+
import { expandPath as expandPathUtil, rawTextResult, textResult } from '@chrischall/mcp-utils';
|
|
2
|
+
// Pretty-printed JSON tool result. Thin wrapper over @chrischall/mcp-utils'
|
|
3
|
+
// `textResult` so the rest of the codebase keeps the local name.
|
|
4
|
+
export const jsonResponse = textResult;
|
|
5
|
+
// Raw-string tool result. Wrapper over @chrischall/mcp-utils' `rawTextResult`.
|
|
6
|
+
export const textResponse = rawTextResult;
|
|
8
7
|
// Translates OFW API recipient shape into the cache's normalized Recipient.
|
|
9
8
|
// Used wherever we surface or persist recipients (sync, get_message, send,
|
|
10
9
|
// save_draft) — all five call sites had near-identical inline mappings.
|
|
@@ -15,11 +14,9 @@ export function mapRecipients(items) {
|
|
|
15
14
|
viewedAt: r.viewed?.dateTime ?? null,
|
|
16
15
|
}));
|
|
17
16
|
}
|
|
18
|
-
// Expand a user-provided path: ~ →
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
22
|
-
}
|
|
17
|
+
// Expand a user-provided path: ~ → home, relative → absolute. Re-exports
|
|
18
|
+
// @chrischall/mcp-utils' `expandPath`.
|
|
19
|
+
export const expandPath = expandPathUtil;
|
|
23
20
|
/**
|
|
24
21
|
* POST a payload to /pub/v3/messages, then immediately GET the detail
|
|
25
22
|
* endpoint for the resulting message id. This is the only correct way to
|
package/dist/tools/messages.js
CHANGED
|
@@ -4,6 +4,7 @@ import { listMessages, countMessages, listDrafts, getMessage, upsertMessage, ups
|
|
|
4
4
|
import { getAttachmentsDir, getDefaultInlineAttachments } from '../config.js';
|
|
5
5
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { basename, dirname, extname, join } from 'node:path';
|
|
7
|
+
import { fileBlob } from '@chrischall/mcp-utils';
|
|
7
8
|
import { expandPath, jsonResponse, mapRecipients, postMessageAndRefetch, textResponse } from './_shared.js';
|
|
8
9
|
// Lightweight mime sniff from extension. OFW re-derives mime from the filename
|
|
9
10
|
// server-side anyway, so this is just a polite Content-Type for the Blob.
|
|
@@ -425,12 +426,12 @@ export function registerMessageTools(server, client) {
|
|
|
425
426
|
const stat = statSync(abs); // throws if missing
|
|
426
427
|
if (!stat.isFile())
|
|
427
428
|
throw new Error(`Not a file: ${abs}`);
|
|
428
|
-
const buf = readFileSync(abs);
|
|
429
429
|
const fileName = basename(abs);
|
|
430
430
|
const mime = mimeFromName(fileName);
|
|
431
431
|
// Build the multipart payload matching the OFW web UI's request shape.
|
|
432
432
|
const form = new FormData();
|
|
433
|
-
|
|
433
|
+
// fileBlob streams the file off disk (a file-backed Blob) instead of buffering it.
|
|
434
|
+
form.append('file', await fileBlob(abs, { type: mime }), fileName);
|
|
434
435
|
form.append('source', 'message');
|
|
435
436
|
form.append('description', args.description ?? fileName);
|
|
436
437
|
form.append('label', args.label ?? fileName);
|
|
@@ -445,7 +446,7 @@ export function registerMessageTools(server, client) {
|
|
|
445
446
|
fileName: meta.fileName ?? fileName,
|
|
446
447
|
label: meta.label ?? args.label ?? fileName,
|
|
447
448
|
mimeType: meta.fileType ?? mime,
|
|
448
|
-
sizeBytes: typeof meta.sizeInBytes === 'number' ? meta.sizeInBytes :
|
|
449
|
+
sizeBytes: typeof meta.sizeInBytes === 'number' ? meta.sizeInBytes : stat.size,
|
|
449
450
|
metadata: meta,
|
|
450
451
|
messageId: 0,
|
|
451
452
|
});
|
|
@@ -453,7 +454,7 @@ export function registerMessageTools(server, client) {
|
|
|
453
454
|
fileId: meta.fileId,
|
|
454
455
|
fileName: meta.fileName ?? fileName,
|
|
455
456
|
mimeType: meta.fileType ?? mime,
|
|
456
|
-
sizeBytes: meta.sizeInBytes ??
|
|
457
|
+
sizeBytes: meta.sizeInBytes ?? stat.size,
|
|
457
458
|
shareClass: meta.shareClass ?? args.shareClass ?? 'PRIVATE',
|
|
458
459
|
note: 'Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it.',
|
|
459
460
|
});
|
|
@@ -476,6 +477,7 @@ export function registerMessageTools(server, client) {
|
|
|
476
477
|
// sentinel — gets re-linked if a message later references this file.
|
|
477
478
|
await fetchAttachmentMeta(client, fileId, 0);
|
|
478
479
|
cached = getAttachment(fileId);
|
|
480
|
+
/* v8 ignore next -- fetchAttachmentMeta persists the row it just fetched; a still-null read here is an unreachable storage failure */
|
|
479
481
|
if (!cached)
|
|
480
482
|
throw new Error(`failed to fetch metadata for fileId ${fileId}`);
|
|
481
483
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"mcpName": "io.github.chrischall/ofw-mcp",
|
|
5
5
|
"description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from \"module\"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js",
|
|
25
25
|
"dev": "node --env-file=.env dist/index.js",
|
|
26
26
|
"test": "vitest run",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
27
28
|
"test:watch": "vitest"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@
|
|
31
|
-
"@fetchproxy/
|
|
31
|
+
"@chrischall/mcp-utils": "^0.9.0",
|
|
32
|
+
"@fetchproxy/bootstrap": "^1.3.0",
|
|
32
33
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
34
|
"dotenv": "^17.4.2",
|
|
34
35
|
"zod": "^4.4.3"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/ofw-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.3.
|
|
9
|
+
"version": "2.3.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.3.
|
|
14
|
+
"version": "2.3.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|