mcp-google-multi 6.0.0-alpha.61 → 6.0.0-alpha.62
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/dist/discovery-client.js +19 -1
- package/dist/http-transport.js +26 -1
- package/dist/mcp-token.d.ts +2 -3
- package/dist/mcp-token.js +38 -8
- package/package.json +1 -1
package/dist/discovery-client.js
CHANGED
|
@@ -74,6 +74,21 @@ export function discoveryCacheDir(env = process.env) {
|
|
|
74
74
|
return (env.DISCOVERY_CACHE_PATH ??
|
|
75
75
|
path.join(env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'mcp-google-multi', 'discovery'));
|
|
76
76
|
}
|
|
77
|
+
// Default cache path only (an operator's DISCOVERY_CACHE_PATH may be shared,
|
|
78
|
+
// even /tmp, and keeps its mode), only when this process owns it, and best
|
|
79
|
+
// effort. Clears group/other bits and keeps setuid/setgid/sticky.
|
|
80
|
+
function tightenOwnDir(dir) {
|
|
81
|
+
if (process.platform === 'win32')
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
const st = fs.statSync(dir);
|
|
85
|
+
if (st.uid === process.getuid?.() && (st.mode & 0o077) !== 0)
|
|
86
|
+
fs.chmodSync(dir, st.mode & ~0o077 & 0o7777);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// the cache still works with the directory's own mode
|
|
90
|
+
}
|
|
91
|
+
}
|
|
77
92
|
// A poisoned cache file could repoint baseUrl off-Google; refuse to send the
|
|
78
93
|
// user's Bearer token anywhere but a googleapis.com host.
|
|
79
94
|
export function isGoogleApiUrl(url) {
|
|
@@ -135,6 +150,7 @@ export async function loadMethodIndex(api, deps = {}) {
|
|
|
135
150
|
throw new Error(`Unknown api "${api}". Known: ${Object.keys(SUPPORTED_APIS).join(', ')}`);
|
|
136
151
|
}
|
|
137
152
|
const fetchFn = deps.fetchFn ?? ((url) => fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }));
|
|
153
|
+
const defaultDir = deps.cacheDir === undefined && process.env.DISCOVERY_CACHE_PATH === undefined;
|
|
138
154
|
const cacheDir = deps.cacheDir ?? discoveryCacheDir();
|
|
139
155
|
const cacheFile = path.join(cacheDir, `${api}.json`);
|
|
140
156
|
const readCache = () => {
|
|
@@ -172,7 +188,9 @@ export async function loadMethodIndex(api, deps = {}) {
|
|
|
172
188
|
if (!res.ok)
|
|
173
189
|
throw new Error(`HTTP ${res.status}`);
|
|
174
190
|
doc = (await res.json());
|
|
175
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
191
|
+
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
|
|
192
|
+
if (defaultDir)
|
|
193
|
+
tightenOwnDir(cacheDir);
|
|
176
194
|
fs.writeFileSync(cacheFile, JSON.stringify(doc), { mode: 0o600 });
|
|
177
195
|
break;
|
|
178
196
|
}
|
package/dist/http-transport.js
CHANGED
|
@@ -66,6 +66,16 @@ function stopReading(socket) {
|
|
|
66
66
|
socket.pause();
|
|
67
67
|
socket.resume = () => socket;
|
|
68
68
|
}
|
|
69
|
+
// For a log line: the pathname only (a query can carry a code or a state),
|
|
70
|
+
// and never a throw, since the caller is the catch-all.
|
|
71
|
+
function pathOf(req) {
|
|
72
|
+
try {
|
|
73
|
+
return new URL(req.url ?? '/', 'http://localhost').pathname;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return '?';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
69
79
|
export class HttpTransportHost {
|
|
70
80
|
opts;
|
|
71
81
|
httpServer;
|
|
@@ -140,8 +150,23 @@ export class HttpTransportHost {
|
|
|
140
150
|
res.once('close', settle);
|
|
141
151
|
// Never rejects: close() waits on it, and a catch-all that itself threw
|
|
142
152
|
// must neither reject close() nor surface as an unhandled rejection.
|
|
153
|
+
// The thrown message can name server paths (a lock timeout does), so it
|
|
154
|
+
// goes only to the host's log.
|
|
143
155
|
const done = this.handle(req, res)
|
|
144
|
-
.catch((e) =>
|
|
156
|
+
.catch((e) => {
|
|
157
|
+
try {
|
|
158
|
+
this.fail(res, 500, 'internal_error', 'internal error');
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
// A throwing log must not reach the destroy below: the answer is sent.
|
|
162
|
+
try {
|
|
163
|
+
this.log(`500 internal_error path=${pathOf(req)}: ${e instanceof Error ? e.message : 'non-Error throw'}`);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// nowhere left to report it
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})
|
|
145
170
|
.catch(() => void res.destroy());
|
|
146
171
|
this.handling.add(done);
|
|
147
172
|
void done.finally(() => this.handling.delete(done));
|
package/dist/mcp-token.d.ts
CHANGED
|
@@ -110,8 +110,7 @@ export declare class RefreshStore {
|
|
|
110
110
|
sub: string;
|
|
111
111
|
} | null;
|
|
112
112
|
/** Drop every ACTIVE record minted under `sub` (linear scan under the store
|
|
113
|
-
* lock)
|
|
114
|
-
*
|
|
115
|
-
* of active tokens dropped. */
|
|
113
|
+
* lock); the save then drops the spent entries of every family left with no
|
|
114
|
+
* active token. Returns the number of active tokens dropped. */
|
|
116
115
|
purgeTenant(sub: string): number;
|
|
117
116
|
}
|
package/dist/mcp-token.js
CHANGED
|
@@ -155,11 +155,42 @@ export class RefreshStore {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
save(data) {
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
// A family with no active token has nothing left to revoke. Over the cap,
|
|
159
|
+
// the subject holding the most spent tokens loses its oldest (ties: the
|
|
160
|
+
// oldest entry), so a subject's rotations, across all its families, only
|
|
161
|
+
// push out its own history (docs/internals.md). Insertion order = rotation
|
|
162
|
+
// order.
|
|
163
|
+
const owner = new Map();
|
|
164
|
+
for (const r of Object.values(data.active))
|
|
165
|
+
owner.set(r.family, r.sub);
|
|
166
|
+
const order = Object.keys(data.spent);
|
|
167
|
+
const held = new Map();
|
|
168
|
+
let total = 0;
|
|
169
|
+
order.forEach((t, i) => {
|
|
170
|
+
const sub = owner.get(data.spent[t]);
|
|
171
|
+
if (sub === undefined) {
|
|
172
|
+
delete data.spent[t];
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const h = held.get(sub);
|
|
176
|
+
if (h)
|
|
177
|
+
h.at.push(i);
|
|
178
|
+
else
|
|
179
|
+
held.set(sub, { at: [i], head: 0 });
|
|
180
|
+
total += 1;
|
|
181
|
+
});
|
|
182
|
+
while (total > SPENT_CAP) {
|
|
183
|
+
let most = { at: [], head: 0 };
|
|
184
|
+
let mostN = 0;
|
|
185
|
+
for (const h of held.values()) {
|
|
186
|
+
const n = h.at.length - h.head;
|
|
187
|
+
if (n > mostN || (n === mostN && n > 0 && h.at[h.head] < most.at[most.head])) {
|
|
188
|
+
most = h;
|
|
189
|
+
mostN = n;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
delete data.spent[order[most.at[most.head++]]];
|
|
193
|
+
total -= 1;
|
|
163
194
|
}
|
|
164
195
|
atomicWriteFileSync(this.path, encryptToken(data, this.masterKey), 0o600);
|
|
165
196
|
}
|
|
@@ -212,9 +243,8 @@ export class RefreshStore {
|
|
|
212
243
|
});
|
|
213
244
|
}
|
|
214
245
|
/** Drop every ACTIVE record minted under `sub` (linear scan under the store
|
|
215
|
-
* lock)
|
|
216
|
-
*
|
|
217
|
-
* of active tokens dropped. */
|
|
246
|
+
* lock); the save then drops the spent entries of every family left with no
|
|
247
|
+
* active token. Returns the number of active tokens dropped. */
|
|
218
248
|
purgeTenant(sub) {
|
|
219
249
|
return withFileLock(this.path, () => {
|
|
220
250
|
const data = this.load();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
3
|
+
"version": "6.0.0-alpha.62",
|
|
4
4
|
"mcpName": "io.github.bakissation/mcp-google-multi",
|
|
5
5
|
"description": "Local MCP server for Google Workspace (Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Search Console, +Forms/Chat/Admin) across multiple accounts — OAuth-only, encrypted token storage, deny-by-default writes.",
|
|
6
6
|
"type": "module",
|