mcp-google-multi 5.1.0-beta.1 → 5.1.1-alpha.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/dist/client.js +2 -3
- package/dist/token-store.d.ts +1 -0
- package/dist/token-store.js +89 -3
- package/dist/tools/calendar.d.ts +2 -2
- package/dist/tools/google-api.js +8 -8
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { google } from 'googleapis';
|
|
2
2
|
import { ACCOUNT_CONFIG } from './accounts.js';
|
|
3
|
-
import { readToken,
|
|
3
|
+
import { readToken, updateToken } from './token-store.js';
|
|
4
4
|
export async function getClient(account) {
|
|
5
5
|
const config = ACCOUNT_CONFIG[account];
|
|
6
6
|
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
|
|
@@ -15,8 +15,7 @@ export async function getClient(account) {
|
|
|
15
15
|
}
|
|
16
16
|
oauth2Client.setCredentials(tokenData);
|
|
17
17
|
oauth2Client.on('tokens', (tokens) => {
|
|
18
|
-
|
|
19
|
-
writeToken(account, { ...existing, ...tokens });
|
|
18
|
+
updateToken(account, tokens);
|
|
20
19
|
});
|
|
21
20
|
return oauth2Client;
|
|
22
21
|
}
|
package/dist/token-store.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare function encryptToken(data: object, masterKey: string): string;
|
|
|
4
4
|
export declare function decryptToken(fileContents: string, masterKey: string): TokenData;
|
|
5
5
|
export declare function readToken(alias: string): TokenData | null;
|
|
6
6
|
export declare function writeToken(alias: string, data: object): void;
|
|
7
|
+
export declare function updateToken(alias: string, updates: object): void;
|
|
7
8
|
export declare function hasToken(alias: string): boolean;
|
package/dist/token-store.js
CHANGED
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { ACCOUNT_CONFIG } from './accounts.js';
|
|
5
5
|
const ENC_VERSION = 1;
|
|
6
6
|
const ALGO = 'aes-256-gcm';
|
|
7
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
8
|
+
const LOCK_RETRY_MS = 10;
|
|
7
9
|
export function deriveKey(masterKey) {
|
|
8
10
|
if (!masterKey) {
|
|
9
11
|
throw new Error('MASTER_KEY is required to encrypt/decrypt tokens. Generate one with: openssl rand -base64 32');
|
|
@@ -56,10 +58,94 @@ export function readToken(alias) {
|
|
|
56
58
|
}
|
|
57
59
|
return decryptToken(contents, masterKey());
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
function sleep(ms) {
|
|
62
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
63
|
+
}
|
|
64
|
+
function withTokenLock(alias, fn) {
|
|
65
|
+
const p = ACCOUNT_CONFIG[alias].encPath;
|
|
66
|
+
const dir = path.dirname(p);
|
|
67
|
+
const lock = path.join(dir, `.${path.basename(p)}.lock`);
|
|
68
|
+
const ownerFile = `${lock}.${process.pid}.${randomBytes(6).toString('hex')}.owner`;
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
fs.writeFileSync(ownerFile, String(process.pid), { mode: 0o600, flag: 'wx' });
|
|
71
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
72
|
+
try {
|
|
73
|
+
while (true) {
|
|
74
|
+
try {
|
|
75
|
+
fs.linkSync(ownerFile, lock);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const err = error;
|
|
80
|
+
if (err.code !== 'EEXIST')
|
|
81
|
+
throw error;
|
|
82
|
+
try {
|
|
83
|
+
const observedOwner = fs.readFileSync(lock, 'utf8');
|
|
84
|
+
const owner = Number(observedOwner);
|
|
85
|
+
if (Number.isSafeInteger(owner) && owner > 0) {
|
|
86
|
+
try {
|
|
87
|
+
process.kill(owner, 0);
|
|
88
|
+
}
|
|
89
|
+
catch (ownerError) {
|
|
90
|
+
if (ownerError.code !== 'ESRCH')
|
|
91
|
+
throw ownerError;
|
|
92
|
+
if (fs.readFileSync(lock, 'utf8') === observedOwner)
|
|
93
|
+
fs.rmSync(lock, { force: true });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (readError) {
|
|
99
|
+
if (readError.code === 'ENOENT') {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
throw readError;
|
|
103
|
+
}
|
|
104
|
+
if (Date.now() >= deadline) {
|
|
105
|
+
throw new Error(`Timed out waiting for token lock: ${alias}`, { cause: error });
|
|
106
|
+
}
|
|
107
|
+
sleep(LOCK_RETRY_MS);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
fs.rmSync(ownerFile, { force: true });
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
return fn();
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
fs.rmSync(lock, { force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function writeTokenAtomic(alias, data) {
|
|
60
122
|
const p = ACCOUNT_CONFIG[alias].encPath;
|
|
61
|
-
|
|
62
|
-
|
|
123
|
+
const dir = path.dirname(p);
|
|
124
|
+
const tmp = path.join(dir, `.${path.basename(p)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
|
|
125
|
+
try {
|
|
126
|
+
fs.writeFileSync(tmp, encryptToken(data, masterKey()), { mode: 0o600, flag: 'wx' });
|
|
127
|
+
const fd = fs.openSync(tmp, 'r');
|
|
128
|
+
try {
|
|
129
|
+
fs.fsyncSync(fd);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
fs.closeSync(fd);
|
|
133
|
+
}
|
|
134
|
+
fs.renameSync(tmp, p);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
fs.rmSync(tmp, { force: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function writeToken(alias, data) {
|
|
141
|
+
withTokenLock(alias, () => writeTokenAtomic(alias, data));
|
|
142
|
+
}
|
|
143
|
+
export function updateToken(alias, updates) {
|
|
144
|
+
withTokenLock(alias, () => {
|
|
145
|
+
const existing = readToken(alias) ?? {};
|
|
146
|
+
const definedUpdates = Object.fromEntries(Object.entries(updates).filter(([, value]) => value !== null && value !== undefined));
|
|
147
|
+
writeTokenAtomic(alias, { ...existing, ...definedUpdates });
|
|
148
|
+
});
|
|
63
149
|
}
|
|
64
150
|
export function hasToken(alias) {
|
|
65
151
|
return fs.existsSync(ACCOUNT_CONFIG[alias].encPath);
|
package/dist/tools/calendar.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare function registerCalendarTools(server: ToolRegistry): void;
|
|
|
3
3
|
export declare function formatEvent(event: any, opts?: {
|
|
4
4
|
full?: boolean;
|
|
5
5
|
}): {
|
|
6
|
+
[k: string]: any;
|
|
7
|
+
} | {
|
|
6
8
|
id: any;
|
|
7
9
|
summary: any;
|
|
8
10
|
description: string;
|
|
@@ -17,6 +19,4 @@ export declare function formatEvent(event: any, opts?: {
|
|
|
17
19
|
hangoutLink: any;
|
|
18
20
|
created: any;
|
|
19
21
|
updated: any;
|
|
20
|
-
} | {
|
|
21
|
-
[k: string]: any;
|
|
22
22
|
};
|
package/dist/tools/google-api.js
CHANGED
|
@@ -16,18 +16,18 @@ const SERVICE_FOR_ALIAS = {
|
|
|
16
16
|
calendar: 'calendar',
|
|
17
17
|
sheets: 'sheets',
|
|
18
18
|
docs: 'docs',
|
|
19
|
-
slides:
|
|
19
|
+
slides: 'slides',
|
|
20
20
|
forms: 'forms',
|
|
21
21
|
people: 'contacts',
|
|
22
22
|
searchconsole: 'searchconsole',
|
|
23
23
|
tasks: 'tasks',
|
|
24
24
|
chat: 'chat',
|
|
25
25
|
meet: 'meet',
|
|
26
|
-
driveactivity:
|
|
27
|
-
drivelabels:
|
|
26
|
+
driveactivity: 'driveactivity',
|
|
27
|
+
drivelabels: 'drivelabels',
|
|
28
28
|
admin_directory: 'admin',
|
|
29
29
|
admin_reports: 'admin',
|
|
30
|
-
groupssettings:
|
|
30
|
+
groupssettings: 'groupssettings',
|
|
31
31
|
};
|
|
32
32
|
function buildQueryString(queryParams) {
|
|
33
33
|
const usp = new URLSearchParams();
|
|
@@ -61,15 +61,15 @@ function describeMethod(m) {
|
|
|
61
61
|
export function registerEscapeTools(registry, policy, deps = {}) {
|
|
62
62
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
63
63
|
const toolsets = deps.toolsets ?? getToolsets();
|
|
64
|
+
const serviceForAlias = (alias) => SERVICE_FOR_ALIAS[alias] ?? alias;
|
|
64
65
|
const apiEnabled = (alias) => {
|
|
65
|
-
|
|
66
|
-
return service === null || service === undefined || toolsetEnabled(toolsets, service);
|
|
66
|
+
return toolsetEnabled(toolsets, serviceForAlias(alias));
|
|
67
67
|
};
|
|
68
68
|
const enabledApis = Object.keys(WORKSPACE_APIS).filter(apiEnabled);
|
|
69
69
|
const apiList = enabledApis.join(', ');
|
|
70
70
|
const toolsetDisabled = (alias) => jsonResult({
|
|
71
71
|
error: 'toolset_disabled',
|
|
72
|
-
message: `API "${alias}" maps to service "${
|
|
72
|
+
message: `API "${alias}" maps to service "${serviceForAlias(alias)}", which is excluded by GOOGLE_TOOLSETS.`,
|
|
73
73
|
hint: 'Add the service to GOOGLE_TOOLSETS (or unset it) to use this API.',
|
|
74
74
|
retriable: false,
|
|
75
75
|
}, true);
|
|
@@ -159,7 +159,7 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
159
159
|
}, true);
|
|
160
160
|
}
|
|
161
161
|
const cud = cudFromMethod(method);
|
|
162
|
-
const policyService =
|
|
162
|
+
const policyService = serviceForAlias(api);
|
|
163
163
|
const lastSegment = method.id.split('.').pop() ?? method.id;
|
|
164
164
|
const toolRef = { name: `${policyService}_${lastSegment}`, service: policyService, cud };
|
|
165
165
|
if (cud !== 'read' && !isAllowed(toolRef, policy)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.1-alpha.1",
|
|
4
4
|
"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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|